-
Notifications
You must be signed in to change notification settings - Fork 73
API documentation
Using the standardised OpenAPI (formerly Swagger) specification we can write API definitions that are auto-converted into interactive documentation.
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}`]),
+ ),
},
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 inapi/
-
api/*/*Router.js
- JSDoc comments in any router file in a directory inapi/
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: