This document outlines a method for creating OpenAPI Specification (OAS) files using a build technique similar to how static site generators (SSGs) build their sites. The goals are fairly simple:
-
Goal: We want to write our CommonMark-formatted, user-facing API documentation in actual Markdown files (not JSON or YAML).
Motivation: If you've ever worked on large OAS-based API docs before, you're very likely to have encountered
description
entries like this. This is a YAML file containing embedded Markdown, which in turn contains some embedded HTML. We'd like to avoid this for many of the same reasons that static websites aren't simply built from HTML: it's easier to write and maintain documentation in dedicated markup files. -
Goal: We want to employ automated linting (spelling and style checks) of our content to ensure that we have mistake-free and on-brand documentation.
Motivation: One of the common issues with typical OAS spec maintenance is the lack of quality assurance (QA) options. Since prose is often embedded in YAML or JSON, it can be difficult to spell check, lint, and format. We want to be able to use the same CI-based testing techniques we use on our static site content.
-
Goal: We want to automate the creation of our OAS spec from our linted content.
Motivation: Since we're now viewing our OAS spec as a read-only output artifact, we need a way to automate its creation from our static API documentation.
-
Goal: We want to be able to deploy our rendered API docs with the output of any static site generator.
Motivation: Since we're now developing our API docs using the same toolchain and workflow as our static site, we want to be able to deploy them in the same way and at the same time using tools like Swagger UI.
Essentially, we're replacing the typical SSG workflow of markup -> HTML
with markup -> OAS -> HTML
. While this may seem unusual to some (especially those that already generate their OAS spec from source code) the idea of writing an API's spec "first" (i.e., by hand rather than from existing source code) isn't new: the concept is commonly referred to as "spec-first development" and has become increasingly popular. We're simply making the process easier to integrate with static site generators and markup-related tooling.
As with any SSG, the key to this workflow is using a consistent and standardized file structure. The exact details will depend on which SSG you'd like to integrate your API docs with, but we'll be using Docusaurus for the purposes of this document.
The basic structure of a Docusaurus site is given below:
├── Dockerfile
├── docker-compose.yml
├── docs
│ ├── doc1.md
│ ...
└── website
├── README.md
├── blog
│ ├── 2016-03-11-blog-post.md
│ ...
├── core
│ └── Footer.js
├── package.json
├── pages
│ └── en
│ ├── help.js
│ ├── index.js
│ └── users.js
├── sidebars.json
├── siteConfig.js
├── static
│ ├── css
│ │ └── custom.css
│ └── img
│ ├── docusaurus.svg
│ ├── favicon
│ │ └── favicon.ico
│ ├── favicon.png
│ └── oss_logo.png
└── yarn.lock
In the static
directory, we create an api
sub-directory that contains Swagger UI's dist
contents:
NOTE [Docusaurus]: Instead of including
dist/css
, we include the required CSS via a CDN in ourindex.html
file:<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.23.10/swagger-ui.css" integrity="sha256-O3WBPOmqAcumrGlHJtGp6KtuKA4S9sbEu3NCSbacxZ8=" crossorigin="anonymous" />If you'd like to include a local copy of the CSS, you'll need to update
siteConfig.separateCss
in your Docusaurus config file.
website/static/api
├── img
│ ├── favicon-16x16.png
│ └── favicon-32x32.png
├── index.html
├── js
│ ├── swagger-ui-bundle.js
│ ├── swagger-ui-standalone-preset.js
│ ...
├── oauth2-redirect.html
├── spec.yml [added to .gitignore]
├── src
│ ...
└── template.yml
The first two files we need to discuss are index.html
and spec.yml
. The index.html
file is pretty standard, except for its reference to spec.yml
:
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "spec.yml", // ADD THIS
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
For our purposes, the spec.yml
file is a read-only build artifact (see Build Process below) and should be added to your .gitignore
file (or equivalent).
template.yml
should contain all non-generated sections of your specification:
openapi: 3.0.2
servers:
- url: http://127.0.0.1:7777
tags:
- name: Linting and Suggestions
description: Find errors and receive possible solutions
- name: Local Resources
description: Get information about the active project and local Vale resources
externalDocs:
description: Vale Server user documentation
url: https://errata-ai.github.io/vale-server/docs/about
components:
schemas:
Suggestions:
type: array
items:
type:
string
Action:
type: object
required:
- Name
- Params
properties:
Name:
type: string
Params:
type: array
items:
type: string
Alert:
type: object
required:
- Action
- Check
- Description
- Line
- Link
- Message
- Severity
- Span
- Match
properties:
Action:
$ref: "#/components/schemas/Action"
Check:
type: string
Description:
type: string
Line:
type: integer
Link:
type: string
Message:
type: string
Severity:
type: string
Span:
type: array
items:
type: string
Match:
type: string
Alerts:
type: array
items:
$ref: "#/components/schemas/Alert"
As you can see, it's a very abbreviated version of a typical spec that mostly consists of non-prose content that doesn't change often.
The src/
directory is where most of our writing will take place. It includes our info
section (info.md
), parameters
(parameters/
), and paths
(endpoints/
):
website/static/api/src
├── endpoints
│ ├── path
│ │ └── get.md
│ ├── suggest
│ │ └── post.md
│ └── vale
│ └── post.md
├── info.md
└── parameters
├── alert.md
├── format.md
└── text.md
From here, the process should be similar to how most SSGs generate their content: we write in Markdown files that use YAML-formatted front matter for metadata.
Here's what our info
section would look in YAML:
info:
title: Vale Server API
version: 1.0.0
contact:
email: support@errata.ai
description: |-
The Vale Server API provides a means of communicating with the Vale Server desktop application, which manages user settings and interfaces with the Vale CLI tool, from third-party "client" applications:
<img src="/vale-server/img/flow.svg" alt="An illustration of Vale Server's API flow." style="margin: auto; min-width: 50%; display: block;">
All of the official Vale Server clients—[Atom][1], [Sublime Text][2], [Visual Studio Code][3], [Google Docs][5], and [Google Chrome][4]—use this API to communicate with the core desktop application.
**NOTE**: Unlike most production APIs, the Vale Server API is embedded within the desktop application itself and runs on `localhost`. This means that users don't have to send their text to a remote server, but it also means that **you'll have to have an instance of Vale Server running to test the API here**.
[1]: https://github.com/errata-ai/vale-atom
[2]: https://github.com/errata-ai/SubVale
[3]: https://github.com/errata-ai/vale-vscode
[4]: https://errata-ai.github.io/vale-server/docs/chrome
[5]: https://errata-ai.github.io/vale-server/docs/gdocs
However, instead of trying to edit and maintain this embedded Markdown in our spec itself, we write the description in /api/src/info.md
:
---
title: Vale Server API
version: 1.0.0
contact:
email: support@errata.ai
---
The Vale Server API provides a means of communicating with the Vale Server desktop application, which manages user settings and interfaces with the Vale CLI tool, from third-party "client" applications:
<img src="/vale-server/img/flow.svg" alt="An illustration of Vale Server's API flow." style="margin: auto; min-width: 50%; display: block;">
All of the official Vale Server clients—[Atom][1], [Sublime Text][2], [Visual Studio Code][3], [Google Docs][5], and [Google Chrome][4]—use this API to communicate with the core desktop application.
**NOTE**: Unlike most production APIs, the Vale Server API is embedded within the desktop application itself and runs on `localhost`. This means that users don't have to send their text to a remote server, but it also means that **you'll have to have an instance of Vale Server running to test the API here**.
[1]: https://github.com/errata-ai/vale-atom
[2]: https://github.com/errata-ai/SubVale
[3]: https://github.com/errata-ai/vale-vscode
[4]: https://errata-ai.github.io/vale-server/docs/chrome
[5]: https://errata-ai.github.io/vale-server/docs/gdocs
This file is much easier to write, update, and lint using our typical writing environment.
The next step is documenting our parameters
. These are often shared between multiple endpoints using references, so we want to define them in their own files. Here's an example (api/src/parameters/format.md
):
---
name: format
in: formData
schema:
type: string
---
The would-be file extension of `text`. In other words, since `text` is passed as a buffer (and not a file path), `format` informs Vale Server of how it should parse the provided content.
This value should include any leading "." characters, as is common practice with extension-extraction utilities such as [`path.extname(path)`](https://nodejs.org/api/path.html#path_path_extname_path) for Node.js:
```js
path.extname('index.coffee.md');
// Returns: '.md'
//
// This is the expected value for `format`.
```
The auto-generated YAML then becomes:
format:
name: format
in: formData
schema:
type: string
description: |-
The would-be file extension of `text`. In other words, since `text` is passed as a buffer (and not a file path), `format` informs Vale Server of how it should parse the provided content.
This value should include any leading "." characters, as is common practice with extension-extraction utilities such as [`path.extname(path)`](https://nodejs.org/api/path.html#path_path_extname_path) for Node.js:
```js
path.extname('index.coffee.md');
// Returns: '.md'
//
// This is the expected value for `format`.
```
The final step is documenting our endpoints. The path structure for these are api/src/endpoints/<NAME>/<METHOD>
—e.g., api/src/endpoints/suggest/post.md
:
---
summary: Retrieve suggestions to fix a given Alert
parameters:
- $ref: '#/components/parameters/alert'
tags:
- Linting and Suggestions
produces:
- application/json
responses:
200:
description: An array of suggestions
content:
application/json:
schema:
$ref: '#/components/schemas/Suggestions'
400:
description: Missing parameter
content:
application/json:
schema:
type: object
required:
- error
properties:
error:
type: string
enum:
- "missing 'alert'"
operationId: FindSuggestions
---
The `/suggest` endpoint accepts a `/vale`-generated Alert and returns an array of possible fixes for the error, warning, or suggestion. The array will be empty if no fixes are found.
Also, while the response of `/vale` depends on the user's configuration, the response of `/suggest` is deterministic: the same suggestions will *always* be returned for a particular Alert.
The generated YAML then becomes:
/suggest:
post:
summary: Retrieve suggestions to fix a given Alert
parameters:
- $ref: '#/components/parameters/alert'
tags:
- Linting and Suggestions
produces:
- application/json
responses:
200:
description: An array of suggestions
content:
application/json:
schema:
$ref: '#/components/schemas/Suggestions'
400:
description: Missing parameter
content:
application/json:
schema:
type: object
required:
- error
properties:
error:
type: string
enum:
- missing 'alert'
operationId: FindSuggestions
description: |-
The `/suggest` endpoint accepts a `/vale`-generated Alert and returns an array of possible fixes for the error, warning, or suggestion. The array will be empty if no fixes are found.
Also, while the response of `/vale` depends on the user's configuration, the response of `/suggest` is deterministic: the same suggestions will *always* be returned for a particular Alert.
To tie this altogether, we use a Python script to generate our final OAS3-compliant specification. We can then easily incorporate our API docs into an existing CI test suite, such as the .travis.yml
example given below:
script:
# Lint our product and API docs using Vale:
- ./bin/vale docs website/static/api/src
after_success:
# Generate our OAS3 spec:
- python3 ci/scripts/api.py
# Publish our docs
...
This will lint both our product and API docs using Vale, and then publish all of our docs together. You can find the entire repository source and published results at errata-ai/vale-server
and https://errata-ai.github.io/vale-server/api/index
, respectively.