Documenting Express REST APIs with OpenAPI and JSDoc
Essential Randomness
Posted on July 18, 2021
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!
Updates
- 7/20/21: Added "documenting models" section.
Goals & Constraints
- 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.
Final Result
Architecture Flow
Documentation Page
How To
Packages used
-
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.
Configuration (Express)
OpenAPI Options
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: "ms.boba@bobaboard.com",
},
},
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"],
};
Documenting Models
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"],
},
]
}
Add the OpenAPI endpoint
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);
});
Component 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
Endpoint Documentation
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
}
Configuration (Docusaurus)
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,
},
},
},
],
],
}
Posted on July 18, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.