Skip to content

API documentation

Jonathan Sharpe edited this page Sep 14, 2024 · 1 revision

Using the standardised OpenAPI (formerly Swagger) specification we can write API definitions that are auto-converted into interactive documentation.

Setup

Install the relevant dependencies:

npm install --workspace api swagger-{jsdoc,ui-express}

Create api/docs/docsRouter.js containing the router:

import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { Router } from "express";
import swaggerJSDoc from "swagger-jsdoc";
import { serve, setup } from "swagger-ui-express";

const __dirname = dirname(fileURLToPath(import.meta.url));

const packageFile = JSON.parse(
	await readFile(join(__dirname, "..", "..", "package.json"), "utf-8"),
);

const docsRouter = Router();

docsRouter.use("/", serve);

docsRouter.get(
	"/",
	setup(
		swaggerJSDoc({
			apis: [
				join(__dirname, "..", "*", "*.yaml"),
				join(__dirname, "..", "*", "*Router.js"),
			],
			definition: {
				info: {
					description: packageFile.description,
					license: packageFile.license,
					title: packageFile.name,
					version: packageFile.version,
				},
				openapi: "3.1.0",
			},
			failOnErrors: true,
		}),
	),
);

export default docsRouter;

Also create api/docs/schema.yaml for reusable definitions, containing:

---
tags:
  - name: Messages
    description: Messages for the user

components:
  responses:
    InternalServerError:
      description: Something went wrong
      content:
        text/plain:
          schema:
            type: string
            examples:
              - Internal Server Error

Update api/app.js to import:

  import db from "./db.js";
+ import docsRouter from "./docs/docsRouter.js";
  import config from "./utils/config.cjs";

and use the router:

  if (config.production) {
  	app.enable("trust proxy");
  	app.use(httpsOnly());
  }
+ 
+ app.use("/docs", docsRouter);
  
  app.get(
  	"/healthz",

For consistency between production and development mode the Vite dev server should proxy /docs to the server alongside /api and /healthz; update web/vite.config.js as follows:

+ const apiEndpoints = ["/api", "/docs", "/healthz"];
  const apiPort = process.env.API_PORT ?? "3100";
        server: {
                port: process.env.PORT,
-               proxy: {
-                       "/api": `http://localhost:${apiPort}`,
-                       "/healthz": `http://localhost:${apiPort}`,
-               },
+               proxy: Object.fromEntries(
+                       apiEndpoints.map((endpoint) => [endpoint, `http://localhost:${apiPort}`]),
+               ),
        },

Usage

The array apis in the configuration defines the files that will be treated as API definitions, in this case:

  • api/*/*.yaml - content of any YAML file in a directory in api/
  • api/*/*Router.js - JSDoc comments in any router file in a directory in api/

For example, the default GET /api/messages could be documented as follows in api/messages/messageRouter.js:

import { Router } from "express";

import { asyncHandler } from "../utils/middleware.js";

import { getMessage } from "./messageService.js";

const router = Router();

/**
 * @openapi
 * /api/message:
 *   get:
 *     description: Get a message from the database
 *     tags:
 *       - Messages
 *     responses:
 *       200:
 *         content:
 *           text/plain:
 *             schema:
 *               type: string
 *               examples:
 *                 - Hello, world!
 *       500:
 *         $ref: "#/components/responses/InternalServerError"
 */
router.get(
	"/",
	asyncHandler(async (_, res) => {
		res.send(await getMessage());
	}),
);

export default router;

This will be displayed as follows:

Screenshot 2024-09-14 at 13 57 44
Clone this wiki locally