-
Notifications
You must be signed in to change notification settings - Fork 74
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:
![Screenshot 2024-09-14 at 13 57 44](https://private-user-images.githubusercontent.com/785939/367516162-f30c5dc6-bf70-4266-b734-90b88a847501.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3NTY4ODMsIm5iZiI6MTczODc1NjU4MywicGF0aCI6Ii83ODU5MzkvMzY3NTE2MTYyLWYzMGM1ZGM2LWJmNzAtNDI2Ni1iNzM0LTkwYjg4YTg0NzUwMS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwMjA1JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDIwNVQxMTU2MjNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04NjM2YmU4MmRmOTgxODRjNGMyYWI0NDlhZWZkNTQ0YjRjMjZkNTc1Y2Y0MDExZGRlYTY3MDU2NGE2ODM5OTQ4JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.znLQsFZSkI8HnD8omS7l8RCqvAdxDSsESaPuW1-nlTY)