Stop Writing Manual API Docs: Automate with Swagger and Express

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

The High Cost of Manual Documentation

During my first year as a backend dev, I spent roughly 20% of every week updating README files and Postman collections. It was a losing battle. The moment a teammate changed a field from a string to an integer without telling me, the documentation became a lie. This led to broken integrations and those dreaded Slack messages: “Hey, why is the /users endpoint returning a 400?”

Manual documentation is a productivity killer. It relies on human memory, which inevitably fails as project complexity grows. OpenAPI (formerly Swagger) fixes this by providing a formal, machine-readable specification for your REST APIs. It allows developers to understand your service’s capabilities without ever touching the source code.

Integrating Swagger UI turns that spec into an interactive playground. In my experience, moving to automated docs reduces developer onboarding time by nearly 50% and completely eliminates the “stale docs” problem. It ensures your documentation is a living reflection of your code.

Installation: Getting Your Tools Ready

To follow along, you need a standard Node.js environment. We will use two specific libraries to bridge the gap between your code and the UI:

  • swagger-jsdoc: This parses your JSDoc comments and converts them into a JSON OpenAPI specification.
  • swagger-ui-express: This hosts the Swagger UI dashboard directly within your Express application.

Let’s set up a fresh project and pull in these dependencies:

mkdir express-swagger-demo
cd express-swagger-demo
npm init -y
npm install express swagger-ui-express swagger-jsdoc

I also recommend adding nodemon. It automatically restarts your server when you update documentation comments, saving you hundreds of manual restarts over the life of a project:

npm install --save-dev nodemon

Configuration: Connecting Swagger to Express

The real advantage of this setup is proximity. Your documentation lives right next to the logic it describes. Start by creating server.js and setting up the basic Express boilerplate.

1. Basic Boilerplate

const express = require('express');
const swaggerJsDoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const app = express();
app.use(express.json());

const PORT = process.env.PORT || 3000;

2. Defining the Swagger Options

The configuration object acts as the “identity card” for your API. It defines the version, title, and the specific files swagger-jsdoc should scan for comments.

const swaggerOptions = {
  swaggerDefinition: {
    openapi: '3.0.0',
    info: {
      title: 'Customer Management API',
      version: '1.0.0',
      description: 'Express API documentation generated via Swagger',
      contact: {
        name: 'Backend Team',
        url: 'https://itfromzero.com'
      },
      servers: [{ url: 'http://localhost:3000' }]
    },
  },
  // Point this to the files containing your API definitions
  apis: ['server.js', './routes/*.js'], 
};

const swaggerDocs = swaggerJsDoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));

3. Documenting Routes with JSDoc

Now we get to the practical work. Instead of maintaining a separate YAML file, we use @openapi tags directly above our route handlers. This keeps the documentation and the code in perfect sync.

/**
 * @openapi
 * /customers:
 *   get:
 *     summary: Retrieve a list of customers
 *     responses:
 *       200:
 *         description: A JSON array of customer objects
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 type: object
 *                 properties:
 *                   id: { type: integer }
 *                   name: { type: string }
 */
app.get('/customers', (req, res) => {
  res.status(200).send([
    { id: 1, name: 'John Doe' },
    { id: 2, name: 'Jane Smith' }
  ]);
});

/**
 * @openapi
 * /customers:
 *   post:
 *     summary: Create a new customer
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               name: { type: string }
 *     responses:
 *       201:
 *         description: Customer created successfully
 */
app.post('/customers', (req, res) => {
  const { name } = req.body;
  res.status(201).send({ message: `Customer ${name} created` });
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
  console.log(`Docs available at http://localhost:${PORT}/api-docs`);
});

Using the @openapi tag makes your code self-documenting. When a new engineer joins the team, they can read the route logic and the API contract at the same time.

Verification: Testing the Interactive UI

Run your server with node server.js and open http://localhost:3000/api-docs. You will see a professional interface that lists every endpoint you’ve documented.

The “Try It Out” Feature

The most powerful part of Swagger UI is the “Try it out” button. It allows you to enter parameters and send real requests to your server without leaving the browser. This effectively replaces the need for shared Postman collections, which often get lost or become outdated.

Scaling with Reusable Schemas

As your API grows to 20 or 50 endpoints, server.js will become messy. To keep things clean, define reusable components. This follows the DRY (Don’t Repeat Yourself) principle and makes updates much faster.

/**
 * @openapi
 * components:
 *   schemas:
 *     Customer:
 *       type: object
 *       required: [name]
 *       properties:
 *         id:
 *           type: integer
 *           description: Auto-generated ID
 *         name:
 *           type: string
 *           description: Full name of the customer
 */

Reference this schema in any route using $ref. If you ever need to add a phone_number field to the Customer object, you only have to change it in one place.

// In your route definition
responses:
  200:
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Customer'

Production Safety and Best Practices

While Swagger UI is essential for development, you should be careful about exposing it in production. For public-facing APIs, it’s a great feature. For internal microservices, however, you might want to hide it behind authentication or disable it based on your environment.

if (process.env.NODE_ENV !== 'production') {
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
}

Automating your documentation transforms it from a chore into a natural part of the development lifecycle. When I review Pull Requests, I treat JSDoc updates with the same importance as the code itself. If the logic changes but the docs don’t, the PR doesn’t get merged. This discipline ensures your documentation remains the ultimate source of truth for your entire team.

Share: