How-to-build-an-OpenAPI-definition-file-step-by-step

How to Build an OpenAPI Definition File (Step by Step)

Posted in

OpenAPI definition files are key for an effective API. An OpenAPI definition file is like a blueprint, technical spec, and road map all in one. OpenAPI descriptions tell machines what to expect when they encounter an API. In API-first scenarios, OpenAPI definition files can even generate the APIs themselves. In this case, it becomes even more important that an OpenAPI definition file is correct.

There are plenty of OpenAPI definition generators out there. However, the problem with automated code generators is that users might not understand what they’re doing. Even if you have experience writing OpenAPI description files by hand, it’s possible to get rusty and forget the nuances of the specification. It’s always a good idea to brush up on your OpenAPI skills to learn the latest features and best practices to implement in your descriptions.

To this end, we’ve put together this guide on how to build an OpenAPI definition file from scratch with nothing more than a text editor. By the time you’re done reading, you’ll have a thorough understanding of each field and how to assemble an API definition from top to bottom.

Step 1: Info & Servers

Now, let’s walk through how to create the OpenAPI definition file step-by-step. We’ll be making a sample definition that describes how a simple to-do list API operates.

First, in your text editor of choice, create a file named openapi.yaml in the root directory. You can also make a JSON file if you prefer that format, but we’ll be using YAML.

In the blank YAML file, start by declaring what version of OpenAPI you’re using. We’re using OpenAPI 3.1.1. Then, give your API a title, version number, and description.

openapi: 3.1.1
info:
  title: ToDo API
  description: A simple API to manage a to-do list
  version: 1.0.0

As you can see above, the OpenAPI description file starts by defining what version of OpenAPI is used. We’re using OpenAPI 3.1.1.

Next, it defines the name of the API in the title field. In this case, this API is named ToDo API.

The description field describes what the API does at a high level, making it readable by humans as well as machines. This API manages a simple to-do list.

The version field allows you to declare the version — in this case, 1.0.0. This is useful when it’s time to handle multiple versions or deprecate an API.

Next, you’re going to define the server’s base URL:

servers:
  - url: https://api.example.com/v1

Finally, the server field allows developers to specify the URL that will serve as the API’s base.

Step 2: Set up Your Paths

Next, the paths section of an OpenAPI description describes each of the API’s endpoints and how to interact with them.

The OpenAPI YAML file we’re creating defines a basic CRUD API, so each endpoint might accept multiple HTTP requests. The /todos endpoint accepts both get and post requests.

Let’s take a look at the get description first to understand how each section should be formatted.

   get:
      summary: List all to-dos
      operationId: listTodos
      responses:
        '200':
          description: A list of to-do items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Todo'

Let’s break this down field by field. First, summary is similar to the description, defining what the command does.

The operationID gives the command a name — in this case listTodos.

The responses section defines each HTTP response code. This example only includes a 200 status code, but each HTTP request method could describe as many HTTP status codes as you like.

The content section lets the machine know what type of asset to expect. This particular response status tells the consumer to expect a JSON file containing an array.

The items segment specifies that the returned data will be formatted using the schema contained at $ref: '#/components/schemas/Todo'.

Now, let’s briefly consider the post request. It’s similar to the get request, but it’s slightly different as the endpoint will be accepting a resource as well as returning an asset.

post:
  summary: Create a new to-do
  operationId: createTodo
  requestBody:
    required: true
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/TodoInput'
  responses:
    '201':
      description: The created to-do
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Todo'

This lets the system know that a POST request to the \todos endpoint must contain a requestBody. If it does, the returned JSON file will be formatted using the same schema as the get request.

Step 3: Consider Other Media Types and Data Types

JSON isn’t the only format supported by OpenAPI 3.1.1. Your API could also return assets using the following:

 text/plain; charset=utf-8
  application/json
  application/vnd.github+json
  application/vnd.github.v3+json
  application/vnd.github.v3.raw+json
  application/vnd.github.v3.text+json
  application/vnd.github.v3.html+json
  application/vnd.github.v3.full+json
  application/vnd.github.v3.diff
  application/vnd.github.v3.patch

The text/plain; charset=utf-8 option lets the machine know that the data won’t be formatted in any way. It will just be raw text.

The application/json option is the most common, though, as it specifies that the JSON file will be interpreted by an application. The other options let the application know that the API will be using some custom media type, which allows users fine-grained control over the returned media.

Schemas offer even more opportunities to structure returned data, allowing users to specify what objects are expected for different fields and then return the results in a consistent format.

Step 4: Adding Authentication To An OpenAPI Definition File

Most APIs will require some form of authentication. You can specify what type of authentication your API accepts in the OpenAPI description with a few lines of code.

The security section is just another component, similar to schemas. Here is our example:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

This lets the system know to expect an authorization token, most likely a JSON Web Token (JWT), from the user.

You can add authentication to specific endpoints, as well. For example, if you wanted to add authentication to a post method, like the one described above, you simply have to add the following:

     security:
        - bearerAuth: []

Common types of authentication methods include:

Type Description Example scheme
http Basic, Bearer, Digest auth basic, bearer
apiKey API key in header, query, or cookie n/a
oauth2 OAuth 2.0 flows authorizationCode, etc
openIdConnect OpenID Connect URL discovery URL required

Step 5: Define Schemas

Finally, you’re going to create the schemas components that will format the query and response.

  schemas:
    Todo:
      type: object
      properties:
        id:
          type: string
          example: "abc123"
        title:
          type: string
        completed:
          type: boolean
      required:
        - id
        - title
        - completed

    TodoInput:
      type: object
      properties:
        title:
          type: string
        completed:
          type: boolean
      required:
        - title
        - completed

Step 6: See The Full Result

Now that you have all the sections written, you can save this file as openapi.yaml.

Here is the complete example of our ToDo OpenAPI definition written in YAML, with some additional fields added:

openapi: 3.1.1
info:
  title: ToDo API
  description: A simple API to manage a to-do list
  version: 1.0.0

servers:
  - url: https://api.example.com/v1

paths:
  /todos:
    get:
      summary: List all to-dos
      operationId: listTodos
      responses:
        '200':
          description: A list of to-do items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Todo'

    post:
      summary: Create a new to-do
      operationId: createTodo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoInput'
      responses:
        '201':
          description: The created to-do
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'

  /todos/{id}:
    get:
      summary: Get a single to-do by ID
      operationId: getTodo
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested to-do item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'
        '404':
          description: To-do not found

    put:
      summary: Update a to-do
      operationId: updateTodo
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoInput'
      responses:
        '200':
          description: The updated to-do
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'
        '404':
          description: To-do not found

    delete:
      summary: Delete a to-do
      operationId: deleteTodo
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: To-do deleted successfully
        '404':
          description: To-do not found

components:
  schemas:
    Todo:
      type: object
      properties:
        id:
          type: string
          example: "abc123"
        title:
          type: string
        completed:
          type: boolean
      required:
        - id
        - title
        - completed

    TodoInput:
      type: object
      properties:
        title:
          type: string
        completed:
          type: boolean
      required:
        - title
        - completed

It’s worth reiterating that OpenAPI definition files can be written in either YAML or JSON. We wrote our definition as a YAML file, as it’s a little bit easier to write by hand.

Below is an example of the exact ToDo OpenAPI definition in JSON, just to give you an idea of what it looks like.

{
  "openapi": "3.1.1",
  "info": {
    "title": "ToDo API",
    "description": "A simple API to manage a to-do list",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.example.com/v1"
    }
  ],
  "paths": {
    "/todos": {
      "get": {
        "summary": "List all to-dos",
        "operationId": "listTodos",
        "responses": {
          "200": {
            "description": "A list of to-do items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Todo"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a new to-do",
        "operationId": "createTodo",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TodoInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created to-do",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Todo"
                }
              }
            }
          }
        }
      }
    },
    "/todos/{id}": {
      "get": {
        "summary": "Get a single to-do by ID",
        "operationId": "getTodo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested to-do item",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Todo"
                }
              }
            }
          },
          "404": {
            "description": "To-do not found"
          }
        }
      },
      "put": {
        "summary": "Update a to-do",
        "operationId": "updateTodo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TodoInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated to-do",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Todo"
                }
              }
            }
          },
          "404": {
            "description": "To-do not found"
          }
        }
      },
      "delete": {
        "summary": "Delete a to-do",
        "operationId": "deleteTodo",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "To-do deleted successfully"
          },
          "404": {
            "description": "To-do not found"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Todo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "abc123"
          },
          "title": {
            "type": "string"
          },
          "completed": {
            "type": "boolean"
          }
        },
        "required": ["id", "title", "completed"]
      },
      "TodoInput": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "completed": {
            "type": "boolean"
          }
        },
        "required": ["title", "completed"]
      }
    }
  }
}

Step 7: Test Your OpenAPI Description

Next comes testing. Testing is a critical step to ensure your definition conforms to the OpenAPI specification and behaves as expected.

To test your OpenAPI description file, you can deploy the openapi.yaml file with a test server to make sure it’s working as it should. To deploy the server, start by installing Prism, an open-source command-line tool from Stoplight for deploying mock servers.

npm install -g @stoplight/prism-cli

Now, you can create a mock server with Prism.

prism mock "C:\Users\jfore\Documents\Programming\test-api\openapi.yaml"

Once that’s running, you can test the endpoint with the following command:

CURL http://127.0.0.1:4010/todos

If everything’s working as it should, this should return the following response:

[{"id":"abc123","title":"string","completed":true}]

Final Thoughts on Creating OpenAPI Definition Files

For the sake of demonstration, in this piece, we wrote our OpenAPI from scratch to get a complete understanding of how OpenAPI operates. But of course, there are plenty of OpenAPI description generators online, most notably Swagger’s OpenAPI Editor, which provides you with a template to base your work on. Such tools will also automatically flag problems in your definitions, which is useful.

That said, the fact that high-quality APIs can be written by hand using free software should dispel any notion that you need expensive tools to create great tech. Even better, crafting your own OpenAPI descriptions lets you get ‘under the hood,’ so to speak, building your API from the ground up, letting you understand each component and how they all work together. It demystifies the process, making it abundantly clear that it’s not hard to create a great API that’s safe, secure, efficient, and useful.