Maintaining accurate and up-to-date API documentation is a common challenge for developers, especially when working in teams. Manually updating Swagger files for each API route can lead to frequent merge conflicts and inconsistencies between the actual API behavior and the documentation. These issues not only slow down development but can also lead to confusion and errors when using the API.
The solution? Automate the generation of Swagger documentation using Joi validation schemas in Node.js with Express. By doing this, you ensure that your documentation stays in sync with your codebase, while eliminating the need for manual updates.
In this article, we will walk through:
Let’s break it down step by step.
When developing an API, it's common to manually update Swagger documentation for each route. In large teams, this leads to conflicts when multiple developers edit the same file, often resulting in lost changes, inconsistent documentation, and wasted time resolving merge conflicts.
By automating Swagger documentation generation using Joi, we can solve this problem. Joi allows us to validate incoming requests to ensure they match our expectations, and at the same time, we can use those same Joi schemas to generate Swagger documentation automatically.
This approach ensures that documentation is always accurate, in sync with the codebase, and reflects the actual structure of the API.
The first step is creating middleware that validates incoming requests using Joi. This middleware ensures that the request's parameters, headers, body, and query strings are valid according to the schema you define. Additionally, the same Joi schema will be used to automatically generate Swagger documentation.
Here’s the middleware:
const Joi = require("joi"); const schemasEnum = Object.freeze({ BODY: 'body', PARAMS: 'params', QUERY: 'query', HEADERS: 'headers' }); const validationMiddleware = (schema) => { return (request, response, nextFunction) => { const validationOptions = { abortEarly: false, allowUnknown: true, stripUnknown: true }; const schemas = Object.values(schemasEnum); const validationResults = {}; for (const key of schemas) { if (schema[key]) { const { error, value } = schema[key].validate(request[key], validationOptions); if (error) { return response.status(400).json({ error: `Validation error in ${key}: ${error.details.map((x) => x.message).join(', ')}` }); } else { validationResults[key] = value; } } } Object.assign(request, validationResults); nextFunction(); }; }; module.exports = { validationMiddleware };
This middleware validates incoming requests based on the schema defined for each route. If the validation fails, it returns an error; otherwise, it allows the request to proceed.
Next, we create a function that registers routes and automatically generates Swagger documentation using the joi-to-swagger library. This function applies the validation middleware and generates the Swagger documentation at the same time.
Here’s how the registerRoute function works:
const { validationMiddleware } = require("../middlewares/validationMiddelware"); const convert = require("joi-to-swagger"); const routes = []; const registerRoute = (router, { method, path, schema = {}, handler, middlewares = [], summary, description, tags = [] }) => { if (schema && Object.keys(schema).length > 0) { middlewares.unshift(validationMiddleware(schema)); } router[method](path, ...middlewares, handler); const parameters = []; const requestBody = {}; if (schema.params) { const { swagger: paramsSwagger } = convert(schema.params); parameters.push(...Object.keys(paramsSwagger.properties).map(key => ({ name: key, in: 'path', required: schema.params._flags.presence === 'required', schema: paramsSwagger.properties[key], description: "paramsSwagger.properties[key].description" }))); } if (schema.query) { const { swagger: querySwagger } = convert(schema.query); parameters.push(...Object.keys(querySwagger.properties).map(key => ({ name: key, in: 'query', required: schema.query._flags.presence === 'required', schema: querySwagger.properties[key], description: "querySwagger.properties[key].description" }))); } if (schema.body) { const { swagger: bodySwagger } = convert(schema.body); requestBody.content = { 'application/json': { schema: bodySwagger } }; } routes.push({ path, method, summary, description, tags, parameters, requestBody }); }; module.exports = { registerRoute, routes };
This function registers the route, applies the Joi validation middleware, and generates Swagger documentation for the route automatically. The joi-to-swagger library is responsible for converting the Joi schema into Swagger format.
Now, let’s define the Joi validation schema for a route. In this example, we define a schema for depositing a balance. It validates the userId parameter and the amount in the request body.
Here’s the Joi schema:
const Joi = require("joi"); const { appErrorSchema } = require("../../../shared/infra/http/schemas/AppErrorSchema"); const depositBalanceParamsSchema = Joi.object({ userId: Joi.number().integer().required().description("User id"), }); const depositBalanceBodySchema = Joi.object({ amount: Joi.number().positive().greater(0).required().description("Amount to deposit"), }); const depositBalanceResponsesSchema = { 204: Joi.any().empty(), 400: appErrorSchema, 404: appErrorSchema, }; module.exports = { depositBalanceParamsSchema, depositBalanceBodySchema, depositBalanceResponsesSchema };
In this schema:
Now that we have the schema, let’s create a route that validates the incoming request and automatically generates Swagger documentation:
const { Router } = require("express"); const { DepositBalanceController } = require("../../../useCases/depositBalance/DepositBalanceController"); const { registerRoute } = require("../../../../shared/infra/http/routes/registerRoute"); const { depositBalanceParamsSchema, depositBalanceBodySchema, depositBalanceResponsesSchema } = require("../../../useCases/depositBalance/DepositBalanceSchema"); const balancesRouter = Router(); const depositBalanceController = new DepositBalanceController(); registerRoute(balancesRouter, { description: "\"Deposit balance\"," handler: depositBalanceController.handle, method: "post", path: "/balances/deposit/:userId", summary: "Deposit balance", schema: { params: depositBalanceParamsSchema, body: depositBalanceBodySchema, responses: depositBalanceResponsesSchema }, tags: ["Balances"] }); module.exports = { balancesRouter };
This route handles deposit requests and validates them using the Joi schema defined earlier. The Swagger documentation is automatically generated based on the Joi schema.
To serve the Swagger documentation, we aggregate the registered routes and generate the Swagger document based on the route definitions.
const { routes } = require("../routes/registerRoute"); const swaggerDocument = { openapi: '3.0.0', info: { title: "'API Documentation'," version: '1.0.0' }, paths: {} }; routes.forEach(route => { const path = route.path.replace(/:([a-zA-Z0-9_]+)/g, '{$1}'); if (!swaggerDocument.paths[path]) { swaggerDocument.paths[path] = {}; } swaggerDocument.paths[path][route.method] = { summary: route.summary, description: "route.description," tags: route.tags, parameters: route.parameters, requestBody: route.requestBody }; }); module.exports = { swaggerDocument };
This code dynamically builds the Swagger document using the registered routes.
To complete the implementation, we set up the Express server and include the Swagger documentation endpoint.
Here’s the server.js file:
// server.js const app = require('./app'); init(); async function init() { try { app.listen(3001, () => { console.log('Express App Listening on Port 3001'); }); } catch (error) { console.error(`An error occurred: ${JSON.stringify(error)}`); process.exit(1); } }
And the app.js file:
// app.js require("express-async-errors"); const express = require('express'); const bodyParser = require('body-parser'); const { appRouter } = require("./modules/shared/infra/http/routes/app.routes"); const swaggerUi = require("swagger-ui-express"); const { swaggerDocument } = require("./swagger/swaggerConfig"); const app = express(); app.use(bodyParser.json()); // Use your app's routes app.use(appRouter); // Serve the Swagger documentation app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); module.exports = app;
Once the server is running, you can access the Swagger documentation at:
http://localhost:3001/api-docs
To run the project and set up the automated Swagger documentation, follow these steps:
npm install express joi joi-to-swagger swagger-ui-express body-parser express-async-errors
These libraries are essential for setting up the Express server, validating incoming requests with Joi, and generating Swagger documentation automatically.
node server.js
This will start your Express server and listen on port 3001 (or any other port you specify).
http://localhost:3001/api-docs
This endpoint serves the interactive Swagger UI, allowing you to test and explore your API routes directly in the browser.
Automating Swagger documentation with Joi streamlines the documentation process, eliminates merge conflicts, and ensures that your API documentation is always in sync with your actual code. By using Joi validation schemas to automatically generate Swagger documentation, you save time and improve the accuracy of your API contracts.
With the steps outlined in this article, you can easily integrate automatic Swagger generation into your Node.js projects, making your development workflow smoother and more efficient.
In summary:
This approach provides a robust solution for both API validation and documentation, allowing teams to work more efficiently and with fewer errors.
The above is the detailed content of Automating Swagger Documentation with Joi in Node.js: Simplify Your API Documentation Workflow. For more information, please follow other related articles on the PHP Chinese website!