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.
This article originally appeared at my blog.
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
curlstatements 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.
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.
We will use the current OpenAPI version 3.0 and the YAML data format for defining the API.
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/
The scanner endpoint accepts HTTP
GET requests and requires a
url parameter. It responds with three status codes:
429. Lets build this step-by-step:
- You define the endpoints in a section called
- Each path has at least one HTTP method:
- Each method has a
responseshave 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
schema defines the
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
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'
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'
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
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
object. This object has
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
job, which in itself is an object with
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
You can see the complete example of the lighthouse API spec here.
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.