Yuliya Bagriy
Posted on September 21, 2021
Obviously, API design is much more than writing OpenAPI description doc. First of all, should it even be OpenAPI-based? If yes, using openapi-cli
will make your life a bit easier.
Example API
Let’s imagine you’ve been asked to create an API for Stargate Network and you already did some preliminary analysis.
Stargate network consists of points in space identifiable via special addresses. When you dial an address (like a phone number), a wormhole is established between your gate and a gate at the address. Gates are physical objects, so they can be destroyed or moved to another address.
First, resources:
- Address
- Id
- Is the address accessible? Can we dial it?
- last known position in human terms (some addresses aren’t fixed points in space)
- Symbols used to dial it
- in galaxy and out of galaxy addresses
- unique address if it assigned to the gate
- Available gates at this address
- Stargate
- Id
- Environment: is it located above the surface, underwater, on the surface, or inside the ship?
- State: is the gate functional, destroyed, or buried?
- Address id
Next, operations. Nothing fancy, the bare minimum:
- List all addresses
- Get address info
- Add new gate info
- Get gate info
- Update gate info
Note: Don’t forget, not all APIs should be OpenAPI-based. If I needed to design a real Stargate system, I would go with a combo of something like GraphQL for lookup and event-based APIs for anything else (perhaps described with AsyncAPI).
Let’s create a place to store our OpenAPI description doc: YAML-based, multi-file git repo. You can read more about why’s in the Redocly docs.
The example repo is on the GitHub. At each step of the tutorial you’ll see links to commits as Commit <commit_hash>.
Repository structure
We’re gonna use create-openapi-repo
to generate the initial structure and then tweak it a bit. Run npx create-openapi-repo
inside a directory where you want to generate API structure. One caveat to keep in mind: this tool will attempt to initialize a git repository and make a commit. Commit e5b40a5.
Let’s explore the generated files:
-
docs
directory contains HTML template for reference doc. I’m deleting it, because I don’t need any custom template at the moment. -
.gitignore
set up to ignore our belovednode_modules
anddist
directory used for generating the final OpenAPI description doc. -
.redocly.yaml
is a configuration file foropenapi-cli
. Because I removeddocs
directory, I also need to removereferenceDocs.htmlTemplate
option here. - Don’t forget to update the
LICENSE
. -
package.json
set up with the most common actions. - Take your time to read
README.md
. It explains what actions are available and provides a sample contribution guidelines. In the real project, you probably don’t want to have all guidelines there in one file. - And last, but not least,
openapi
directory. This is where your API definitions live. Almost all subdirectories have a relevantREADME.md
explaining why these particular files were created as well as showing you other alternatives. I don’t want to copy-paste them here, so, go ahead and explore ;)
Now, let’s rewind the time. Stargate Network API contract is defined, commit c2b8d11. For more information about keeping your structure DRY read this and this. If you’re interested in reading/watching about it from me, just ping!
Note: The definitions are not state of art, even for this shallow design. Some problems with it were added consciously to simplify the example, others for fixing in later articles.
And I’m not even talking about how many details are omitted lore-wise!
Preview
In the package.json
file there is already an action for previewing the docs: openapi preview-docs
. To execute it, run in the terminal npm run start
.
This starts a server with the generated Redoc reference documentation (docs). It watches changes from the disk; usually I keep it running while writing definitions and use it like a UI of Stoplight Studio or Insomnia Designer.
As you may (or may not) know, you can customize how Redoc looks like. This is done withreferenceDocs
section in .redocly.yaml
(docs). Beware, some options are available only for premium edition.
Options can be divided into two categories: those that affect a theme (colors, fonts) and those for disabling or enabling certain features. For example, requiredPropsFirst: true
will display required properties before others:
See at commit c873f15.
Linting
From time to time you should lint with openapi lint
(docs). Let’s execute npm run test
.
➜ npm run test
> stargate-network@1.0.0 test /home/aviskase/Projects/openapi-cli-examples
> openapi lint
validating /home/aviskase/Projects/openapi-cli-examples/openapi/openapi.yaml...
[1] openapi/openapi.yaml:1:1 at #/
Servers must be present.
1 | openapi: 3.0.3
2 | info:
… | < 30 more lines >
33 | $ref: components/securitySchemes/api_key.yaml
34 |
Error was generated by the no-empty-servers rule.
/home/aviskase/Projects/openapi-cli-examples/openapi/openapi.yaml: validated in 27ms
❌ Validation failed with 1 error.
run with `--generate-ignore-file` to add all problems to ignore file.
Oops! We have an error! Wait a second, no-empty-servers
rule complains that we must have servers
defined. But OpenAPI specification doesn’t state this as a required field, so our description doc is valid. Where does this error come from?
When .redocly.yaml
was generated, it included lint
section:
lint:
extends:
- recommended
rules:
no-unused-components: warning
The no-empty-servers
comes from extending recommended set of rules. If it’s too strict, you may want to change to minimal
. Or change it to all
to get even more errors.
I prefer keeping recommended rules and explicitly disabling unneeded ones. Per docs you need to set no-empty-servers: off
in rules
section, commit ee2e3ff. Or you can change severity to warning
.
You can read more about other built-in rules in the docs. In the later article I’ll show how to add your own custom rules.
Bundling
Most of the tools require single-file OpenAPI doc. That’s where bundling comes in. The package.json
is preconfigured with the script to run openapi bundle -o dist
which will save a generated file to the dist
directory. Usually it’s enough, but in some cases you may want to run with --derefenreced
flag to produce a file with internally inlined definitions (docs).
➜ npm run build
> stargate-network@1.0.0 build /home/aviskase/Projects/openapi-cli-examples
> openapi bundle -o dist
bundling /home/aviskase/Projects/openapi-cli-examples/openapi/openapi.yaml...
📦 Created a bundle for /home/aviskase/Projects/openapi-cli-examples/openapi/openapi.yaml at dist.yaml 28ms.
Hmm, what just happened? Instead of ./dist/<filename>.yaml
we’ve got ./dist.yaml
. Why?
Well, this is kinda bug or feature situation. If you check the code, this looks like an intended behavior, but nevertheless confusing. In short, if you have only one API definition, -o
seems to be expecting file path. If you have multiple API definitions (more about that in the upcoming part two), this option can point to directory.
In our example we have one API definition, so to fix we should change the script either to -o dist/openapi
or to -o dist/openapi.yaml
. Both will work the same, as long as --ext
equals to yaml
by default. If you want to generate file in JSON format, you can use -o dist/openapi --ext json
or -o dist/openapi.json
. If you want both formats, you need to run commands separately, see at commit a4e1e23.
Generating reference doc
One of the features that openapi-cli
doesn’t have, but I need all the time is to generate static Redoc html file. Yes, you’d want to invest in better solutions and processes at some point, but at smaller scale it’s ok to send a simple HTML file when someone from non-dev team wants to see the latest or prototype API thingy.
First, we need to install redoc-cli
via npm i redoc-cli
. Then, add a new script to package.json
to generate an HTML file inside dist
directory:
"docs": "redoc-cli bundle dist/openapi.yaml -o dist/redoc.html"
After running npm run docs
and opening the generated file you’ll notice that all our custom options are gone. You see, redoc-cli
doesn’t support .redocly.yaml
configuration file. To fix this, add prepareOptions.js
which reads referenceDocs
from .redocly.yaml
and saves it to .redoc.json
Next, modify docs
script to run preparation script and use .redoc.json
:
"docs": "node plugins/prepareOptions.js && redoc-cli bundle dist/openapi.yaml -o dist/redoc.html --options .redoc.json"
See at commit ad6dacc.
IMHO
Personally, I prefer different names for scripts:
-
preview
instead ofstart
-
lint
instead oftest
-
bundle
instead ofbuild
-
build
fornpm run lint && npm run bundle && npm run docs
Then, all I do is jump between npm run preview
and npm run build
.
See at commit 51c078e.
That’s it for today. In the part two, I’ll show how to manage multiple definitions per repository.
Posted on September 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.