Creating a Simple OpenAPI Spec

Having API descriptions in your workflow is a game-changer, but how do you get from nothing to a functioning spec in the first place? I recently made a very simple spec for a very simple API, and thought it might make a good example for anyone looking to create a spec for the first time. OpenAPI specs are very verbose so this very tiny API makes quite a manageable example!

For the impatient: if you just want to look at an example without any additional context, there's a spec here https://github.com/lornajane/flask-planets-and-webhooks/blob/master/openapi.yaml

About the API

I've been using a very trivial API example project in a few of my recent talks on API-adjacent topics, with just a couple of endpoints. There's a blog post about the Planets API but in short you can get a list of either one planet or many, with requests like:

GET http://localhost:5000/planets
GET http://localhost:5000/planets/3

Now you have a decent grasp of what this thing does, I'll start the description.

Create File, Configure Tools

I'll be using YAML, you can use JSON instead if you prefer. My file is called openapi.yaml which is a reasonably common file name but you can name your file what you like.

I never attempt to create a file like this without some sort of validation tool. Something like Stoplight Studio will do this for you, but I mostly use my usual editor so I'm using Stoplight's standalone open source linting tool spectral combined with watch, something like this:

watch -n 3 spectral lint openapi.yaml

Spectral is fairly Opinionated (with an intentional capital O) but it's a decent starting point.

With this in place, I started adding a few lines to the file:

openapi: 3.0.3
info:
  title: Planets and Webhooks Demo API
  version: 0.0.1
  description: Simple flask-backed API showing some example API endpoints and with webhook debugging features.
  contact:
    name: Lorna Mitchell
    url: https://github.com/lornajane/flask-planets-and-webhooks
    email: [email protected]
servers:
  - url: http://localhost:5000
    description: Sample project running locally
externalDocs:
  description: GitHub project for the code and information about this API
  url: https://github.com/lornajane/flask-planets-and-webhooks

You'll probably still see some validation errors, in this version of OpenAPI 3.0.3 it's required to have a paths: item at the top level, but you get the idea of how the start of the file should look.

Add API Endpoints

This API has two endpoints, so it's a nice, small problem space to try to describe! The /planets route returns JSON, an array of objects where each object describes a planet. The /planets/{id} route returns just one object, but it is identical to the object returned by the collection. OpenAPI supports a special $ref field for reusing definitions across different parts of the spec. You can use it to structure your spec in a more readable way - but its superpower is in making sure that things that are the same are only defined once, making it easier to keep everything consistent and maintainable.

I prefer to start with an endpoint without any reuse, then refactor to components later. As an example, here's the /planets/{id} endpoint in its first iteration:

paths:
  /planets/{planetId}:
    get:
      operationId: onePlanet
      summary: Fetch one planet by position
      description: Get the data relating to one planet
      parameters:
      - name: planetId
        in: path
        required: true
        schema:
          type: number
          example: 4
      responses:
        '200':
          description: Planets in a list
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                    description: Name of planet
                    example: "Saturn"
                  position:
                    type: number
                    description: Order in place from the sun
                    example: 6
                  moons:
                    type: number
                    description: Number of moons, according to NASA
                    example: 62

I did say it was verbose! The value is in the detail though - every tool that consumes this description has all this information and examples available.

I know I'll need the actual planet object again though, so here's the same sample again, refactored to put the planet object into components as a reusable element:

paths:
  /planets/{planetId}:
    get:
      operationId: onePlanet
      summary: Fetch one planet by position
      description: Get the data relating to one planet
      parameters:
      - name: planetId
        in: path
        required: true
        schema:
          type: number
          example: 4

      responses:
        '200':
          description: Planets in a list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/planet"

components:
  schemas:
    planet:
      type: object
      properties:
        name:
          type: string
          description: Name of planet
          example: "Saturn"
        position:
          type: number
          description: Order in place from the sun
          example: 6
        moons:
          type: number
          description: Number of moons, according to NASA
          example: 62

With the reusable component ready, have a look at how the collection looks using it:

paths:
  /planets:
    get:
      operationId: allPlanets
      summary: List all planets
      description: Returns a list of all the planets that are stored in the system.
      responses:
        '200':
          description: Planets in a list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/planet"

It's a little different as it's an array of objects, but we can reuse that single object as an array item.

Documentation as Output

There are so many things you can do with an OpenAPI spec but a very human-centric one is to generate documentation. There are a few options for this, again Stoplight Studio has it built in, but I mostly use open source tool ReDoc - their 2.0 release isn't official yet but it is great! Importantly, the project was very receptive to, and quick to fix, the accessibility issues I logged.

screenshot of redoc rendering openapi.yaml

Generating some documentation as you go along can help show how things are going when you come to describe your own APIs.

Final Touches

If you're playing along at home, you'll notice that spectral is still having Opinions! That's because we haven't added tags yet. Tags aren't that useful on an OpenAPI spec this size, but for larger APIs they are an excellent innovation. The previous yaml example showing the collection endpoint has a tag, and to finish, all I need to show you is the tags: element in the spec:

tags:
  - name: planets
    description: API containing solar system data

For tags, parameters, respond fields, endpoints, and really everything else, the descriptions and examples are at least as important as the field name and data type information! Not all of us are talented writers but the best specifications are those that do include these extra-value items - the improved Developer Experience (or as I like to call it "developer delight factor") is absolutely worth it!

Let me know if you found this post useful or if there's anything more detailed you'd like to see covered. I'm still figuring a bunch of these things out myself but I'm happy to share :)


Also published on Medium.

Leave a Reply

Please use [code] and [/code] around any source code you wish to share.

This site uses Akismet to reduce spam. Learn how your comment data is processed.