Skip to content

Instantly share code, notes, and snippets.

@philsturgeon
Last active November 14, 2021 00:08
Show Gist options
  • Save philsturgeon/e11b4cd603666b54d6436de6542998b7 to your computer and use it in GitHub Desktop.
Save philsturgeon/e11b4cd603666b54d6436de6542998b7 to your computer and use it in GitHub Desktop.
OpenAPI specifications style guide / file format

API Specs & Docs

Style Guide

A lot of this information has been used to create linting rules for Speccy, so just run the linter with --rules=strict for advice. It will be updated over time, as new rules are created.

File Structure

OpenAPI supports JSON Pointer via $ref keys, even when you’re writing in YAML. Swagger.io has a great guide for $ref and JSON Pointer.

Warning, if $ref exists then its siblings will be ignored.

Conventions for us are to have:

  • Move the schema objects to api/schemas/<resource-or-collection-name>.json
  • A single error format can be made to shape the object in api/schemas/error.json
  • Make a shared parameters.yml for any common query string params, headers, etc.

Collections vs Resource

When writing the contract for a single resource, try to model the bare attributes and not get to stuck on the wrappers like "result" or "meta", etc. 

To model a Foo, name file api/schemas/foo.json and it might look a bit like this:

{
  "title": "Foo",
  "type": "object",
  "properties": {
    "id": {
      "readOnly": true,
      "type": "string",
      "example": "123"
    },
    ...
  },
  "required": [
    "account_uuid",
    "action",
    "starts_at"
  ]
}

When specifying a collection, if the collection view is just an array of full objects (all the same properties), then you can simply $ref the foo resource in "items" and make a collection api/schemas/foos.json:

{
  "title": "Foos",
  "type": "array",
  "items": {
    "$ref": "foo.json"
  }
}

If the collection view is a subset of the resource, you can cherry-pick the relevant fields:

{
  "type": "array",
    "items": {
      "type": "object",
      "properties": {
        "id": {
          "$ref": "./foo.json#/properties/id"
        },
        "name": {
          "$ref": "./foo.json#/properties/name"
        }
      }
    }
  }
}

Paths

To avoid the openapi.yml getting huge, we recommend splitting the paths into their own files:

Take the URL path, turns slashes into dashes, and you have a decent naming convention for files!

We recommending making a different path file for each different URL you have in the openapi.yml, making collections, resources, and and other sub-collections or sub-resources follow the same convention. For example, /companies/{id}/user-stats would become paths/companies-id-user-stats.yml.

The main openapi.yml can then be simplified to $ref these path files:

paths:
  /v3/companies/{company_id}/user_stats:
    $ref: './paths/v3-companies-id-user-stats.yml'
  /v3/companies/{company_id}/seats:
    $ref: './paths/v3-companies-id-seats.yml'
  /v3/companies/{company_id}/seats/{uuid}:
    $ref: './paths/v3-companies-id-seats-uuid.yml'
  /v3/companies:
    $ref: './paths/v3-companies.yml'
  /v3/companies/{company_id}:
    $ref: './paths/v3-companies-id.yml'

TODO how to handle metadata and responses

Info > Version

Whichever approach to versioning is employed for this API, the info.version field should contain a version number for the specification itself, and these should follow SemVer.

Writing specifications for an API with multiple versions is tough. So far success has been had using the vendor extension x-tagGroups. Check out out Spaceman took care of this in spaceman/pull/3367

Servers

Share links to any and all servers that people might care about, usually thats localhost (so they know the right port number), general staging (no team clusters), and production.

servers:
  - url: https://foo.example.com
    description: Production server
  - url: https://foo-staging.example.com
    description: Staging server
  - url: http://localhost:3004
    description: Development server

Naming Things

Navigation

Tags[ are used for the main navigation sections on the left, then the summary field from the Operation Object[ is used next (more on that below).

Demo of where tags and summary appear in the docs.

/invoices/{id}/items:
  get:
    tags:
    - Invoice Items

You can reference tags arbitrarily, and definition is optional.

Defining tags allows you to add more information like a description.

tags:
  - name: Invoice Items
    description: |+
      Giant long explanation about what this business concept is, because other people _might_ not have a clue!

Operation Object: Summary

The Operation Object summary field is used for the links that users are going to click, and are also considered as a title in most representations. Try to keep them short, otherwise it's going to make a rather tricky looking navigation:

Keep summary short

Place any further information in description, on either the Operation Object or the Tag Object.

For a standardized list of summaries, we strongly recommend sticking to this list:

Foo Collection (/foos):

  • Retrieve Collection
  • Filter Collection
  • Create Resource

Foo Resource (foos/{id}):

  • Retrieve Resource
  • Replace Resource
  • Update Resource
  • Delete Resource

Subresource endpoints:

  • Retrieve {subresource_name} Collection
  • Filter {subresource_name} Collection
  • Retrieve {subresource_name} Resource
  • Create {subresource_name} Resource
  • Replace {subresource_name} Resource
  • Update {subresource_name} Resource
  • Delete {subresource_name} Resource

RPC-style and "not-really-a-resource" endpoints:

  • A few short words (no more than 5) explaining the action

Operation Object: Description

The place for longer form information about how the operation is going to work. You don't need to explain that a PUT or PATCH is an update, but you might want to mention anything that might be considered "unexpected" (especially if the URL is more RPC than REST).

Operation description displayed in documentation

Multiple lines and markdown are all supported, so in YAML use |+.

get:
 summary: Retrieve All
 description: |+
  Fetching invalidations is literally only useful for Spacestation,
  and should not be confused with actual permissions. The lack of an
  invalidation does not mean that a user or account can do something, so
  please do not attempt to use as such!

Operation Object: Operation ID

This operation ID is essentially a reference for the operation, which can be used to visially suggest a connection to other operations. This is like some theoretical static HATEOAS-style referencing, but it's also used for the URL.

Make the value lower-hyphen-case, and try and think of a name for the action which does not relate to the HTTP message. Base it off the actual action. create-polygon? search-by-polygon? filter-companies?

Default and Example

Please provide default and example whenever possible. These values are placed into human readable documentation, and can be used for UI generation, tooltips, etc. You don't always need to provide both, as the default will be used as an example, if the example is not provided.

Automation

It's a bit tough to remember all of this advice, so let's use Speccy to help us out.

If you're using CircleCI, add the following to your .circle/config.yml:

version: 2
jobs:
  # other jobs

  speccy:
    docker:
        - image: circleci/node:8
    steps:
      - checkout
      - run:
          name: Lint
          command: npx speccy lint ./api/openapi.yml --rules=strict

workflows:
  version: 2
  commit:
    jobs:
      # other jobs

      - speccy:
          context: org-global
          requires:
            - build
          filters:
            branches:
              ignore:
                - master

That's it! Now, CircleCI will report a little red X next to your jobs if speccy has found validation or lint errors. Once you've made speccy happy, you can make the speccy job required (GitHub Repo Settings > Branches > master > Protect). This will fail PRs when new issues pop up, to ensure quality only gets better over time.

Next Steps

Follow all the advice in here and from Speccy will help you generate some really useful specifications, and get up to Silver in no time.

You could now put these specs in a branch and/or pass them off to a developer to implement the actual functionality. Inversely, if the functionality already exists and you are adding these specs after the fact, you can use these new specs to contract test your API responses!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment