Documenting Express REST APIs with OpenAPI and JSDoc
Tags: express, documentation, javascript, docusaurus
Published: 2021-07-18 21:25:06

> 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](https://www.bobaboard.com)'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](https://docusaurus.io/) instance hosted on a different server.
- (Not implemented): Ensuring endpoints conform to the documented API. While we could use [express-openapi-validator](https://github.com/cdimascio/express-openapi-validator/), it doesn't currently support OpenAPI 3.1 ([issue](https://github.com/cdimascio/express-openapi-validator/issues/573))
  - *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
![How everything interacts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3f5exhtdj0qzi71nkq27.png)
### Documentation Page
![The BobaBoard Rest API Documentation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d1c30ttwqldflprww9zt.png)

## How To

### Packages used
- [SwaggerJSDoc](https://www.npmjs.com/package/swagger-jsdoc): to turn JSDocs into the final OpenAPI spec (served at `/open-api.json`).
- [Redocusaurus](https://www.npmjs.com/package/redocusaurus): to embed [Redoc](https://github.com/Redocly/redoc) into Docusaurus. There are other options for documentation, like any OpenAPI/Swagger compatible tool (e.g. [SwaggerUI](https://swagger.io/tools/swagger-ui/)), 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](https://swagger.io/specification/#oasObject).
```javascript
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](https://swagger.io/docs/specification/components/) to define reusable models. These are not automatically documented at this stage ([workaround issue](https://github.com/Redocly/redoc/issues/1528)).

To add models documentation, add the following section to your top-level configuration.

```javascript
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.
```javascript
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](https://swagger.io/docs/specification/components/) used throughout the documentation.
`/types/open-api/contribution.yaml`
```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.

```javascript
/**
 * @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](https://www.npmjs.com/package/redocusaurus):
`docusaurus.config.js`:

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