20
Documenting Express REST APIs with OpenAPI and JSDoc
As usual, this article isn't meant as an in-depth guide, but as a documentation of the what, why, and how of certain architectural choices. If you're trying to achieve the same thing and need help, leave a comment!
- 7/20/21: Added "documenting models" section.
- To document BobaBoard's REST API.
- Standardize (and document) both the parameters and the responses of various endpoints.
- The documentation should be as close as possible to the source code it describes.
- The documentation should be served through a docusaurus instance hosted on a different server.
- (Not implemented): Ensuring endpoints conform to the documented API. While we could use express-openapi-validator, it doesn't currently support OpenAPI 3.1 (issue)
- Consideration: at least at first, we'd like to report the discrepancies without failing the requests. I'm unsure whether this is supported by this library.
-
SwaggerJSDoc: to turn JSDocs into the final OpenAPI spec (served at
/open-api.json
). - Redocusaurus: to embed Redoc into Docusaurus. There are other options for documentation, like any OpenAPI/Swagger compatible tool (e.g. SwaggerUI), but Redoc is the nicest feeling one.
These options define the global configuration and settings of your OpenAPI spec. You can find the OpenAPI-specific settings (i.e. the one NOT specific to Redoc) on the OpenAPI website.
const options = {
definition: {
openapi: "3.1.0",
info: {
title: "BobaBoard's API documentation.",
version: "0.0.1",
// Note: indenting the description will cause the markdown not to format correctly.
description: `
# Intro
Welcome to the BobaBoard's backend API. This is still a WIP.
# Example Section
This is just to test that sections work. It will be written better later.
`,
contact: {
name: "Ms. Boba",
url: "https://www.bobaboard.com",
email: "[email protected]",
},
},
servers: [
{
url: "http://localhost:4200/",
description: "Development server",
},
],
// These are used to group endpoints in the sidebar
tags: [
{
name: "/posts/",
description: "All APIs related to the /posts/ endpoints.",
},
{
name: "/boards/",
description: "All APIs related to the /boards/ endpoints.",
},
{
name: "todo",
description: "APIs whose documentation still needs work.",
},
],
// Special Redoc section to control how tags display in the sidebar.
"x-tagGroups": [
{
name: "general",
tags: ["/posts/", "/boards/"],
},
],
},
// Which paths to parse the API specs from.
apis: ["./types/open-api/*.yaml", "./server/*/routes.ts"],
};
OpenAPI specs can contain a Components section to define reusable models. These are not automatically documented at this stage (workaround issue).
To add models documentation, add the following section to your top-level configuration.
const options = {
// ...
tags: [
// ...
{
name: "models",
"x-displayName": "Models",
// Note: markdown must not contain spaces after new line.
description: `
## Contribution
<SchemaDefinition schemaRef="#/components/schemas/Contribution" />
## Tags
<SchemaDefinition schemaRef="#/components/schemas/Tags" />
`,
],
"x-tagGroups": [
{
name: "models",
tags: ["models"],
},
]
}
Configure the Express server to surface your spec through an /open-api.json
endpoint. Redocusaurus will use it to retrieve the data to display.
import swaggerJsdoc from "swagger-jsdoc";
const specs = swaggerJsdoc(options);
app.get("/open-api.json", (req, res) => {
res.setHeader("Content-Type", "application/json");
res.send(specs);
});
Reusable types used throughout the documentation.
/types/open-api/contribution.yaml
# Note the /components/schemas/[component name] hierarchy.
# This is used to refer to these types in the endpoint
# documentation.
components:
schemas:
Contribution:
type: object
properties:
post_id:
type: string
format: uuid
parent_thread_id:
type: string
format: uuid
parent_post_id:
type: string
format: uuid
secret_identity:
$ref: "#/components/schemas/Identity"
required:
- post_id
- parent_thread_id
- secret_identity
This should be repeated for every API endpoint you wish to document.
/**
* @openapi
* posts/{postId}/contribute:
* post:
* summary: Replies to a contribution
* description: Posts a contribution replying to the one with id {postId}.
* tags:
* - /posts/
* - todo
* parameters:
* - name: postId
* in: path
* description: The uuid of the contribution to reply to.
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 403:
* description: User is not authorized to perform the action.
* 200:
* description: The contribution was successfully created.
* content:
* application/json:
* schema:
* type: object
* properties:
* contribution:
* $ref: "#/components/schemas/Contribution"
* description: Finalized details of the contributions just posted.
*/
router.post("/:postId/contribute", isLoggedIn, async (req, res) => {
// The endpoint code
}
You must update your docusaurus configuration after installing Redocusaurus:
docusaurus.config.js
:
module.exports = {
// other config stuff
// ...
presets: [
// other presets,
[
"redocusaurus",
{
specs: [
{
routePath: "docs/engineering/rest-api/",
// process.env.API_SPEC is used to serve from localhost during development
specUrl:
process.env.API_SPEC ||
"[prod_server_url]/open-api.json",
},
],
theme: {
// See options at https://github.com/Redocly/redoc#redoc-options-object
redocOptions: {
expandSingleSchemaField: true,
expandResponses: "200",
pathInMiddlePanel: true,
requiredPropsFirst: true,
hideHostname: true,
},
},
},
],
],
}
20