OpenAPI (formerly Swagger) is an open-source tool to standardize specification of REST APIs. This standardization enables programmatic parsing of an API's structure and syntax to automatically generate documentation, client libraries, and even server code. Think of OpenAPI as the source of truth for your API; from schemas to endpoints to security configurations.
OpenAPI documents are most commonly created as YAML files. You can also use JSON. The resulting file serves as your fundamental text that streamlines production of documentation and client libraries. This saves time and minimizes errors from manually creating reference docs and API wrappers.
There are numerous ways to create an OpenAPI spec. OpenAPI.tools is probably the most comprehensive list of tools to help you. Spotlight and Postman are two of the best UI tools to create your spec.
You can also, of course, do it the hard way. In this guide, we'll take inspiration from Zed Shaw over at Learn Code The Hard Way by writing an OpenAPI YAML file by hand. This is obviously more time-consuming and error prone, but sometimes if you want to understand something, doing it the hard, manual way is the best way. Once you understand it at a deeper level, you become a better conductor of the tools that abstract some of the inner workings.
What We're Building
We need an idea for a simple API before we can get started. Let's imagine a simple, CRUD dictionary API that allows user to look up, create, and modify definitions of words. We'll need the following paths:
- POST /definitions
- GET /definitions/{word}
- PUT /definitions/{word}
- DELETE /definitions/{word}
The API will use JSON in request and response bodies.
For security, we'll use Basic Authentication for all endpoints except GET /definitions.
Initial Setup
Create a file called definitions.yaml
. We'll start by defining the version of OpenAPI we're using (3.0.0) and same basic description info
of our API. The info.version
field refers to the version of our API.
# definitions.yaml
openapi: 3.0.0
info:
title: Dictionary REST API
description: An API to look up definitions of words.
version: 1.0.0
Next we'll add our API's base URL under servers
. You can add multiple servers if you want to support production and development instances. We'll stick to one for now.
We'll also define our security scheme of Basic Auth here as well under components
. Doing this defines a security scheme that we can then apply to each endpoint that needs this security scheme.
# definitions.yaml
openapi: 3.0.0
info:
title: Dictionary REST API
description: An API to look up definitions of words.
version: 1.0.0
servers:
- url: https://sample-api.apimaestro.com
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
If we wanted to set Basic Auth as our global security scheme and apply it to all endpoints, we could do that under security
like the example below. We'll leave this out of our spec to keep GET /definitions unprotected.
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
security:
- basicAuth: []
Schemas
Data schemas are defined under components
. You can set a title
, description
, and type
. This content will appear in generated reference documentation.
The type
field can be a string, number, integer, boolean, array, or object. You can also define multiple types for a single field.
We'll create a definition schema of type object.
# definitions.yaml
openapi: 3.0.0
...
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
schemas:
Definition:
title: Definition
description: A definition is a statement that explains the meaning of a word or phrase.
type: object
The object's schema will be referenced when we're setting request and response bodies for our endpoints. We can define the fields that belong to our object under properties
. In the example below, I'll define the following:
id
as a read-only integerword
as a stringcreated_at
andupdated_at
as read-only strings in thedate-time
formatmeanings
as an array of objects
The meanings array will allow us to handle words with multiple definitions. We could split out meanings as its own schema model, but for now, we can define its schema inline with its parent object.
Our meanings object will have the following properties:
- id as a read-only integer
part_of_speech
as a stringmeaning
as a string
# definitions.yaml
openapi: 3.0.0
...
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
schemas:
Definition:
title: Definition
description: A definition is a statement that explains the meaning of a word or phrase.
type: object
properties:
id:
type: integer
example: 40125
readOnly: true
word:
type: string
example: tagliatelle
created_at:
type: string
format: date-time
example: 2022-12-06T04:55:36.887Z
readOnly: true
updated_at:
type: string
format: date-time
example: 2022-12-06T04:55:36.887Z
readOnly: true
meanings:
type: array
items:
type: object
properties:
id:
type: integer
example: 333
readOnly: true
part_of_speech:
type: string
example: noun
meaning:
type: string
example: A type of pasta shaped into long, thin, flat strips
All set here. Now we can start writing our endpoints.
Endpoints
All of our endpoints will go under the paths
section. For each path, we can set multiple methods. Our simple CRUD API has only 2 paths; /definitions for POST and /definitions{word} for GET, PUT, and DELETE.
# definitions.yaml
openapi: 3.0.0
...
paths:
/definitions/{word}:
get:
put:
delete:
/definitions:
post:
Get a Definition
First, we'll add our word look up endpoint.
Similar to schemas, you can add summary
and description
content here that will appear in documentation.
I also recommend setting an operationId
. This is used to set the method name in auto-generated client libraries. We'll set operationId: get_definition
. We can also use camel case operationId: getDefinition
.
OpenAPI is smart enough to accept either snake or camel case and convert appropriately. In Python or Ruby, the method would remain in snake case, api.get_definition(word)
. In camel case languages like Javascript, the method would be api.getDefinition(word)
.
# definitions.yaml
openapi: 3.0.0
...
paths:
/definitions/{word}:
get:
summary: Get a word's definition.
operationId: get_definition
description: > # We can write a multi-line description using the `<` character.
The GET /definitions endpoint accepts a word and returns its meaning.
The endpoint returns multiple meanings if available.
Our GET /definitions endpoint will accept one path parameter. This will be the word we're looking up. We define this under parameters
.
# definitions.yaml
openapi: 3.0.0
...
paths:
/definitions/{word}:
get:
summary: Get a word's definition.
operationId: get_definition
description: > # We can write a multi-line description using the `<` character.
The GET /definitions endpoint accepts a word and returns its meaning.
The endpoint returns multiple meanings if available.
parameters:
- name: word
in: path
description: Word to look up
schema:
type: string
required: true
example: tortellini
Next, we define how this endpoint responds under responses
. We can define multiple responses depending on the response code.
We need to define the response description
and content
. We set the schema
of the response body by referencing the definition schema we created previously.
paths:
/definitions/{word}:
get:
summary: Get a word's definition.
operationId: get_definition
description: > # We can write a multi-line description using the `<` character.
The GET /definitions endpoint accepts a word and returns its meaning.
The endpoint returns multiple meanings if available.
parameters:
- name: word
in: path
description: Word to look up
schema:
type: string
required: true
example: tortellini
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
Let's also add a 404 Not Found response.
paths:
/definitions/{word}:
get:
summary: Gets a definition of a word.
operationId: get_definition
description: > # We can write a multi-line description using the `<` character.
The GET /definitions endpoint accepts a word and returns its meaning.
The endpoint returns multiple meanings if available.
parameters:
- name: word
in: path
description: Word to look up
schema:
type: string
required: true
example: tortellini
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
'404':
description: No definition found for the specified word.
Create a Definition
The structure for creating a POST request doesn't require much more than our GET request. We start again with the basic description information.
/definitions:
post:
summary: Create a word's definition
operationId: create_definition
description: >
POST /definitions creates a definition of a word.
Requests to create a duplicate definitions for a word will be rejected with a 409 response code.
Requests must be authorized with Basic Auth.
Remember we want to protect this endpoint with Basic Auth to prevent anyone from creating definitions. We can do that by adding a security
specification set to basicAuth
.
/definitions:
post:
summary: Create a word's definition
operationId: create_definition
description: >
POST /definitions creates a definition of a word.
Requests to create a duplicate definitions for a word will be rejected with a 409 response code.
Requests must be authorized with Basic Auth.
security:
- basicAuth: []
This time we don't need to set any parameters. Instead, we'll accept a request body. Similar to the response body we set in the GET request, we can reference our schema definition in the same way to set the request body.
/definitions:
post:
summary: Create a word's definition
operationId: create_definition
description: >
POST /definitions creates a definition of a word.
Requests to create a duplicate definitions for a word will be rejected with a 409 response code.
Requests must be authorized with Basic Auth.
security:
- basicAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
required: true
responses:
Finally, we'll add a few response codes and specify that successful creations respond with the full definition.
/definitions:
post:
summary: Create a word's definition
operationId: create_definition
description: >
POST /definitions creates a definition of a word.
Requests to create a duplicate definitions for a word will be rejected with a 409 response code.
Requests must be authorized with Basic Auth.
security:
- basicAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
required: true
responses:
'201':
description: Word created successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
'400':
description: Invalid definition data provided.
'401':
description: Unauthorized request.
'409':
description: Word already has a definition.
'422':
description: Unprocessable entity.
Update a Definition
Things get easy here because we don't need anything new. We can re-use what we set in the GET and POST requests to create our PUT request. We'll copy the POST request and then adjust the following:
- Change the method.
- Update labels and descriptions.
- Copy the parameter specs from the GET request.
- Update response codes.
Here's what the full PUT request should look like.
/definitions/{word}:
put:
summary: Update a word's definition
operationId: update_definition
description: >
PUT /definitions updates a definition of a word.
Requests must be authorized with Basic Auth.
security:
- basicAuth: []
parameters:
- name: word
in: path
description: Word to look up
schema:
type: string
required: true
example: tortellini
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
required: true
responses:
'204':
description: Word updated successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
'400':
description: Invalid definition data provided.
'401':
description: Unauthorized request.
'404':
description: Word not found.
'422':
description: Unprocessable entity.
Delete a Definition
Setting the DELETE request is even more trivial than the PUT request. In this case, we copy the GET request and update the following:
- Change the method.
- Update labels and descriptions.
- Add Basic Auth.
- Update response codes.
Here's what we're left with.
/definitions/{word}:
delete:
summary: Delete a word's definition
operationId: delete_definition
description: >
DELETE /definitions/{word} deletes a definition of a word.
Requests must be authorized with Basic Auth.
security:
- basicAuth: []
parameters:
- name: word
in: path
description: Word to look up
schema:
type: string
required: true
example: tortellini
responses:
'204'
description: Word deleted successfully.
'401':
description: Unauthorized request.
'404':
description: Word not found.
Conclusion
Congratulations. You've created an OpenAPI document the hard way. Of course, you're not going to want to do this a regular exercise. Now that you have a foundation of how OpenAPI works, you're better prepared to use tools like Stoplight to simplify generating an OpenAPI file while also benefitting from their linter.
Finally, here's the full spec we created in a single code block:
openapi: 3.0.0
info:
title: Dictionary REST API
description: An API to look up definitions of words.
version: 1.0.0
servers:
- url: 'https://sample-api.apimaestro.com'
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
schemas:
Definition:
title: Definition
description: A definition is a statement that explains the meaning of a word or phrase.
type: object
properties:
id:
type: integer
example: 40125
readOnly: true
word:
type: string
example: tagliatelle
created_at:
type: string
format: date-time
example: '2022-12-06T04:55:36.887Z'
readOnly: true
updated_at:
type: string
format: date-time
example: '2022-12-06T04:55:36.887Z'
readOnly: true
meanings:
type: array
items:
type: object
properties:
id:
type: integer
example: 333
readOnly: true
part_of_speech:
type: string
example: noun
meaning:
type: string
example: 'A type of pasta shaped into long, thin, flat strips'
paths:
/definitions/{word}:
get:
summary: Get a word's definition.
operationId: get_definition
description: |
GET /definitions/{word} accepts a word and returns its meaning. The endpoint returns multiple meanings if available.
parameters:
- name: word
in: path
description: Word to look up.
schema:
type: string
required: true
example: tortellini
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
'404':
description: No definition found for the specified word.
put:
summary: Update a word's definition
operationId: update_definition
description: |
PUT /definitions updates a definition of a word. Requests must be authorized with Basic Auth.
security:
- basicAuth: []
parameters:
- name: word
in: path
description: Word to look up
schema:
type: string
required: true
example: tortellini
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
required: true
responses:
'204':
description: Word updated successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
'400':
description: Invalid definition data provided.
'401':
description: Unauthorized request.
'404':
description: Word not found.
'422':
description: Unprocessable entity.
delete:
summary: Delete a word's definition
operationId: delete_definition
description: |
DELETE /definitions/{word} deletes a definition of a word. Requests must be authorized with Basic Auth.
security:
- basicAuth: []
parameters:
- name: word
in: path
description: Word to look up
schema:
type: string
required: true
example: tortellini
responses:
'204':
description: Word deleted successfully.
'401':
description: Unauthorized request.
'404':
description: Word not found.
/definitions:
post:
summary: Create a word's definition
operationId: create_definition
description: >
POST /definitions creates a definition of a word.
Requests to create a duplicate definitions for a word will be rejected with a 409 response code.
Requests must be authorized with Basic Auth.
security:
- basicAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
required: true
responses:
'201':
description: Word created successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/Definition'
'400':
description: Invalid definition data provided.
'401':
description: Unauthorized request.
'409':
description: Word already has a definition.
'422':
description: Unprocessable entity.
parameters: []