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.
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 toapi/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.
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"
}
}
}
}
}
To avoid the openapi.yml
getting huge, we recommend splitting the paths into their own 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
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
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
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).
/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!
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:
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
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!
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
?
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.
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.
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!