DEV Community

Cover image for Automating Swagger Documentation with Joi in Node.js: Simplify Your API Documentation Workflow
Gleidson Leite da Silva
Gleidson Leite da Silva

Posted on

Automating Swagger Documentation with Joi in Node.js: Simplify Your API Documentation Workflow

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:

  • Setting up Joi validation and Swagger documentation automation.
  • Solving the problem of manual Swagger updates and merge conflicts.
  • Ensuring robust API contract validation for all routes.

Let’s break it down step by step.


The Problem: Manual Swagger Maintenance

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.

Step 1: Setting Up Joi Validation Middleware

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 };
Enter fullscreen mode Exit fullscreen mode

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.

Step 2: Registering Routes with Joi and Automatically Generating Swagger

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 };
Enter fullscreen mode Exit fullscreen mode

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.

Step 3: Defining Joi Schemas for Routes

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 };
Enter fullscreen mode Exit fullscreen mode

In this schema:

  • The userId must be a required integer.
  • The amount must be a positive number greater than 0.
  • The responses include validation for status codes 204, 400, and 404 using an error schema.

Step 4: Registering the Deposit Balance Route

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 };
Enter fullscreen mode Exit fullscreen mode

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.

Step 5: Generating the Swagger Document

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 };
Enter fullscreen mode Exit fullscreen mode

This code dynamically builds the Swagger document using the registered routes.

Step 6: Setting Up the Express Server

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Once the server is running, you can access the Swagger documentation at:

http://localhost:3001/api-docs
Enter fullscreen mode Exit fullscreen mode

How to Install and Run

To run the project and set up the automated Swagger documentation, follow these steps:

  1. Install Dependencies: Make sure you install all the required libraries, including express, joi, swagger-ui-express, and joi-to-swagger.
   npm install express joi joi-to-swagger swagger-ui-express body-parser express-async-errors
Enter fullscreen mode Exit fullscreen mode

These libraries are essential for setting up the Express server, validating incoming requests with Joi, and generating Swagger documentation automatically.

  1. Run the Server: Once the dependencies are installed, you can run the server using:
   node server.js
Enter fullscreen mode Exit fullscreen mode

This will start your Express server and listen on port 3001 (or any other port you specify).

  1. Access Swagger Documentation: Open your browser and navigate to the following URL to view the automatically generated Swagger documentation:
   http://localhost:3001/api-docs
Enter fullscreen mode Exit fullscreen mode

This endpoint serves the interactive Swagger UI, allowing you to test and explore your API routes directly in the browser.


Conclusion

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:

  • Joi simplifies request validation and ensures that incoming data conforms to the expected format.
  • Swagger documentation is automatically generated from Joi schemas, eliminating manual updates.
  • Developers can focus on writing code without worrying about synchronizing documentation and API contracts.

This approach provides a robust solution for both API validation and documentation, allowing teams to work more efficiently and with fewer errors.

Top comments (0)