In the world of backend development, an API without documentation is like a library without a card catalog—it might contain amazing resources, but nobody knows how to find or use them.
As we settle into 2025, the days of maintaining separate Word documents or manually editing massive YAML files for API specs are effectively over. The ecosystem has matured. Today, Automated Documentation—where your code serves as the single source of truth—is not just a luxury; it’s a requirement for scalable engineering teams.
Whether you are building a microservice for internal consumption or a public-facing API, adhering to the OpenAPI Specification (OAS) is the industry standard.
In this guide, we will move beyond the basics. We will clarify the confusion between Swagger and OpenAPI, implement automated documentation pipelines in both Express and Fastify, and look at how to leverage schemas for validation and documentation simultaneously.
Prerequisites #
Before we dive into the code, ensure your development environment meets the following criteria. We are focusing on modern Node.js development practices.
- Node.js: Version 20 (LTS) or 22 (Current).
- Package Manager:
npmoryarn. - API Client: Postman or Insomnia (to test your endpoints).
- Editor: VS Code (recommended for its excellent IntelliSense support).
We will be using ES Modules (import syntax) throughout this tutorial, as CommonJS is largely legacy in modern 2025 setups.
The Landscape: Swagger vs. OpenAPI #
One of the most common interview questions and points of confusion is the difference between Swagger and OpenAPI. Let’s clear that up immediately.
- OpenAPI Specification (OAS): This is the standard. It is a language-agnostic description format for REST APIs. Think of it like HTML—a specification that defines structure. We are currently focusing on OAS 3.0 and 3.1.
- Swagger: This is the set of tools implemented by SmartBear (and the open-source community) to work with the OpenAPI Specification. This includes Swagger Editor, Swagger UI (the documentation page), and Swagger Codegen.
Comparison of Documentation Strategies #
Choosing the right tool depends heavily on your framework and architectural philosophy.
| Strategy | Tools | Pros | Cons | Best For |
|---|---|---|---|---|
| Comment-Based | swagger-jsdoc |
Easy to add to existing Express apps; keeps docs near code. | Clutters code files; no runtime validation guarantee. | Legacy/Standard Express APIs. |
| Schema-Driven | fastify-swagger, zod |
Single source of truth (Validation + Docs); high performance. | Steeper learning curve; requires specific frameworks (Fastify/NestJS). | Modern, high-performance APIs. |
| Decorator-Based | tsoa, NestJS |
Extremely clean TypeScript integration; strong typing. | High abstraction; “Magic” behavior can be hard to debug. | TypeScript-heavy teams. |
| Manual First | YAML / JSON | Absolute control over the spec; Design-First approach. | Drift happens (Code changes, docs don’t); tedious maintenance. | Public APIs requiring strict contracts. |
The Architecture of Automated Docs #
Before writing code, let’s visualize how an automated documentation pipeline works in a modern Node.js application.
Approach 1: The Express.js Standard (swagger-jsdoc) #
If you are using Express, swagger-jsdoc remains the most popular choice. It reads JSDoc-style comments (annotated with @swagger) right above your routes and generates the JSON spec.
Step 1: Project Setup #
Create a new directory and initialize your project:
mkdir express-swagger-demo
cd express-swagger-demo
npm init -y
# Install dependencies
npm install express swagger-jsdoc swagger-ui-express cors
# Install dev dependencies (optional but recommended)
npm install --save-dev nodemonStep 2: Configuration #
Create a file named swaggerConfig.js. This holds the metadata for your API.
// swaggerConfig.js
import swaggerJsdoc from 'swagger-jsdoc';
const options = {
definition: {
openapi: '3.1.0', // Keeping it modern
info: {
title: 'Node DevPro Express API',
version: '1.0.0',
description: 'A professional API for managing developer resources',
license: {
name: 'MIT',
url: 'https://spdx.org/licenses/MIT.html',
},
contact: {
name: 'Node DevPro Support',
url: 'https://nodedevpro.example.com',
email: '[email protected]',
},
},
servers: [
{
url: 'http://localhost:3000',
description: 'Development server',
},
],
},
// Path to the API docs - this points to your route files
apis: ['./routes/*.js'],
};
export const specs = swaggerJsdoc(options);Step 3: The Application Code #
Create index.js and a routes folder.
// index.js
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import cors from 'cors';
import { specs } from './swaggerConfig.js';
import bookRoutes from './routes/books.js';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// Documentation Endpoint
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, { explorer: true }));
// Routes
app.use('/books', bookRoutes);
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`Documentation available at http://localhost:${PORT}/api-docs`);
});Step 4: Writing Documented Routes #
Now, create routes/books.js. This is where the magic happens. We use YAML syntax inside the comments.
// routes/books.js
import express from 'express';
const router = express.Router();
/**
* @swagger
* components:
* schemas:
* Book:
* type: object
* required:
* - title
* - author
* properties:
* id:
* type: string
* description: The auto-generated id of the book
* title:
* type: string
* description: The title of your book
* author:
* type: string
* description: The book author
* example:
* id: d5fE_asz
* title: The Pragmatic Programmer
* author: Andy Hunt
*/
/**
* @swagger
* tags:
* name: Books
* description: The books managing API
*/
/**
* @swagger
* /books:
* get:
* summary: Returns the list of all the books
* tags: [Books]
* responses:
* 200:
* description: The list of the books
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Book'
*/
router.get('/', (req, res) => {
// Logic to fetch books would go here
res.json([
{ id: '1', title: 'Clean Code', author: 'Robert C. Martin' },
{ id: '2', title: 'Refactoring', author: 'Martin Fowler' }
]);
});
/**
* @swagger
* /books:
* post:
* summary: Create a new book
* tags: [Books]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Book'
* responses:
* 201:
* description: The book was successfully created
* 500:
* description: Some server error
*/
router.post('/', (req, res) => {
const book = req.body;
// Logic to save book
res.status(201).json({ message: "Book created", book });
});
export default router;Pros of this method: It is explicit. You can copy-paste YAML from Swagger Editor directly into your code comments.
Cons: The comments can become longer than the code itself, and there is no guarantee that req.body actually matches the schema you wrote in the comments.
Approach 2: The Modern Schema Way (Fastify + Zod) #
For high-performance applications in 2025, Fastify has gained massive ground against Express. Its best feature? Code-First documentation.
We will use Zod, the current industry standard for schema validation, to define our data structures. Fastify will automatically validate the request and generate the Swagger UI from that same Zod schema.
Step 1: Setup #
mkdir fastify-swagger-demo
cd fastify-swagger-demo
npm init -y
npm install fastify @fastify/swagger @fastify/swagger-ui fastify-type-provider-zod zodStep 2: Implementation #
Notice how much cleaner this is. We don’t write YAML comments; we write actual TypeScript/JavaScript code that runs at runtime.
// server.js
import Fastify from 'fastify';
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUi from '@fastify/swagger-ui';
import { serializerCompiler, validatorCompiler, jsonSchemaTransform } from 'fastify-type-provider-zod';
import { z } from 'zod';
const app = Fastify({ logger: true });
// 1. Setup Zod Compiler
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
// 2. Register Swagger (The OpenApi Generator)
await app.register(fastifySwagger, {
openapi: {
info: {
title: 'Fastify Zod API',
description: 'High performance API with automatic docs',
version: '1.0.0',
},
servers: [{ url: 'http://localhost:3000' }],
},
transform: jsonSchemaTransform, // Auto-convert Zod to JSON Schema
});
// 3. Register Swagger UI (The Visual Interface)
await app.register(fastifySwaggerUi, {
routePrefix: '/documentation',
});
// 4. Define Schemas with Zod
const UserSchema = z.object({
id: z.string().uuid(),
username: z.string().min(3),
email: z.string().email(),
});
const CreateUserSchema = UserSchema.omit({ id: true });
// 5. Define Routes
app.post(
'/users',
{
schema: {
description: 'Create a new user',
tags: ['Users'],
summary: 'Creates user with validation',
body: CreateUserSchema, // Zod schema for validation AND docs
response: {
201: UserSchema, // Zod schema for response documentation
},
},
},
async (req, reply) => {
// If we reach here, req.body is GUARANTEED to be valid
// and correctly typed.
const { username, email } = req.body;
return reply.status(201).send({
id: crypto.randomUUID(),
username,
email,
});
}
);
// Start
try {
await app.listen({ port: 3000 });
console.log('Docs at http://localhost:3000/documentation');
} catch (err) {
app.log.error(err);
process.exit(1);
}Why this wins in 2025: #
- DRY (Don’t Repeat Yourself): You define the
UserSchemaonce. It validates incoming data, provides type safety (if using TS), and generates the Swagger documentation. - Accuracy: You cannot have documentation that says a field is “optional” while your code throws an error if it’s missing. The code is the documentation.
Best Practices for Production #
Generating the docs is step one. Managing them in a professional environment is step two.
1. Security Schemas (Authentication) #
Don’t leave your API Key logic undocumented. In swagger-jsdoc, add this to your definition:
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
security: [{ bearerAuth: [] }],This adds the “Authorize” button to your Swagger UI, allowing developers to paste their JWT token once and test all endpoints.
2. Linting with Spectral #
Just as you lint your JavaScript with ESLint, you should lint your OpenAPI specs with Spectral.
Create a .spectral.yaml file:
extends: ["spectral:oas", "spectral:simple"]Run it against your generated JSON spec. This ensures you aren’t missing descriptions, operation IDs, or using deprecated HTTP methods.
3. Build-Time Generation vs. Runtime #
In the examples above, we generated docs at runtime (when the server starts). For high-traffic production environments, consider generating the swagger.json file during your CI/CD build step and serving it as a static file. This saves CPU cycles on your API server.
Conclusion #
Documentation is often the most neglected part of the software development lifecycle, but modern tooling has lowered the barrier significantly.
If you are maintaining a legacy Express application, integrating swagger-jsdoc allows you to retroactively add documentation without rewriting your router logic. However, if you are starting a new project in 2025, the combination of Fastify and Zod offers a superior developer experience where documentation is a byproduct of good code, not a chore.
Key Takeaways:
- OpenAPI is the specification; Swagger is the toolset.
- Automation prevents “documentation drift” where docs lag behind code.
- Use Zod or similar schema validation libraries to kill two birds with one stone: runtime validation and API specs.
Start implementing these patterns today. Your frontend team (and your future self) will thank you.
Further Reading #
Found this guide helpful? Subscribe to Node DevPro for more deep dives into professional Node.js architecture.