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.
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 APIservers:
- 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
andresponses
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.