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

    29

    This website collects cookies to deliver better user experience

    Documenting Express REST APIs with OpenAPI and JSDoc