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!

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: "[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"],
};

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,
          },
        },
      },
    ],
  ],
}

20