Skip to content

OpenAPI: How to Design API Specifications

By Sebastian Günther

Posted in Tech, Open_api

OpenAPI is a JSON or YAML based specification format. It helps you in designing a detailed specification of your API: endpoints, payloads to use, expected HTML status code. Such a specification gives you the opportunity of using various OpenAPI tools: visualizing your API, performing automatic requests, and even code validation or code generation.

This article gives an introduction to writing an OpenAPI specification. The running example for this endeavor will be the internal API of my lighthouse SAAS. We will see how general metadata, API endpoints and the structure of response/request data can be specified.

OpenAPI Building Blocks

An OpenAPI specification consists of the following elements:

  • Metadata: This section encodes information about the API specification itself, like version, title, author or further links
  • Servers: A list of servers that offer the API you are describing, it is used by some tools to generate curl statements that you can use live to query the API
  • Endpoints: The concrete endpoints of your API, described as complex objects with this information: Path, HTTP methods, parameters (required/optional), and responses with status code, content-type and response
  • Schemas/Definitions: All structured data objects, either used in requests or returned in the response, can be defined in this section and then referenced from other parts within the specification

These are the building blocks. Some nuances come with different versions of OpenAPI: It was formerly called Swagger, and many older projects still use this version. This article covers exclusively the OpenAPI format.

Example: Lighthouse API

Lighthouse is a service to scan webpages and see how good they score on terms of SEO, performance and best practices. You can use the scanner here: https://lighthouse.admantium.com/.

The scanner provides two main endpoints: Starting a scan with /scan, where a url query parameter is passed, and to get the status of a /api/jobs by passing a uuid query parameter. We will see how to specify these two endpoints, their parameters, their response codes and return values/objects.

OpenAPI Declaration for Lighthouse

We will use the current OpenAPI version 3.0 and the YAML data format for defining the API.

Info and Listing Servers

At the beginning of your document, you are providing the essential metadata about the specification, things like which API version you are using, version, title etc.

openapi: 3.0.0
info:
  version: 1.0
  title: Lighthouse API

servers:
  - url: https://lighthouse.admantium.com/

You can also add a list of servers that are hosting your API. This information will be used by some tools to generate code that calls your API directly, or for examples that use the curl command to call the endpoint directly.

servers:
  - url: https://lighthouse.admantium.com/

Scanner Endpoint

The scanner endpoint accepts HTTP GET requests and requires a url parameter. It responds with three status codes: 200, 400 and 429. Lets build this step-by-step:

  • You define the endpoints in a section called paths
  • Each path has at least one HTTP method: get, post, put, option, query, delete
  • Each method has a description, parameters and responses
  • responses have at least one status code that they return.

The first version of the scanner endpoint description looks like this:

paths:
  /scan:
    get:
      description: Initiate a webpage scan
      parameters:
        ...
      responses:
        200:
          ...
        400:
          ...
        429:
          ...

Then, we detail the parameters. First, you define the parameters name. Then, you define where the parameter is in-cluded: In the query, or in the body. The schema defines the type.

paths:
  /scan:
    get:
      description: Initiate a webpage scan
      parameters:
        - name: url
          in: query
          schema:
            type: string

And finally, we give more details about the response. Following the status code, you add the description and its content. The content first lists the mime-type, which can be familiar values such as application/json and text/html. If it is JSON, you can provide further details in the form of a complete schema definition - we will look to it in a later place.

paths:
  /scan:
    get:
      # ...
      responses:
        200:
          description: Webpage scan is accepted and will be processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanResponse'
        400:
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DefaultError'
        429:
          description: Scan requests can not be accepted, all workers are occupied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanResponse'

Job Endpoint

Another endpoint of the application provides the capabilities to check for the status of a scanning job. This endpoint is available at /api/jobs, and it needs to be called with an uuid query parameter.

/api/jobs:
    get:
      description: Get the job status for the provided `uuid`
      parameters:
        - name: uuid
          in: query
          schema:
            type: string
      responses:

The responses are 200 for a completed, 202 for an ongoing job, 409 if the job has an error, and 400 if the request is faulty. Whatever the status code, the mime-type is always application/json. The concrete response payload is, similar to before, detailed as a separate object.

responses:
  200:
    description: 'JOB status: finished'
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/JobResponse'
  202:
    description: 'JOB status: started'
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/JobResponse'
  400:
    description: Invalid request
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/DefaultError'
  409:
    description: 'JOB status: error'
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/JobResponse'

Defining Request or Response JSON Payload Data Structures

Lets go back to the /scan endpoint and its return result. Below the mime-type definition of application/json, we see a $ref definition, which is a pointer to an existing specification. In this particular example, the pointer reads as #/components/schemas/ScanResponse, which literally means "in this file, at components/schemas, use the ScanResponse datatype".

responses:
  200:
    description: Webpage scan is accepted and will be processed
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/ScanResponse'

Lets see the details of this schema definition. It starts with the name ScanResponse, and its type is object. This object has properties: a string msg, a integer code, and a string uuid. You can even define regular-expressions to further specify the structure of individual attributes.

components:
  schemas:
    ScanResponse:
      type: object
      properties:
        msg:
          type: string
        code:
          type: integer
        uuid:
          type: string

The definition of the JobResponse datatype is more complex because it has an inherent structure: Its properties are msg, code and job, which in itself is an object with uuid, domain, status and record. This complexity is simplified in an OpenAPI specification because you just embed the job declaration. Here is the result:

JobResponse:
  type: object
  properties:
    msg:
      type: string
    code:
      type: integer
    job:
      type: object
      properties:
        uuid:
          type: string
        domain:
          type: string
        status:
          type: string
        record:
          type: boolean

Complete Example

You can see the complete example of the lighthouse API spec here.

Summary

With OpenAPI, you can specify your API in its details: endpoints, parameter, request/response objects. In a concise, declarative language you define these elements. The specification model can then be used by various tools: documentation viewer, validators, and even code generators. In the next article, you can read more about this tool chain.