API Documentation with Swagger
Undocumented APIs are a tax on every developer who uses them. They create support tickets, guessing games, and integration bugs that could have been avoided with a single well-maintained spec file. Swagger — the toolset built around the OpenAPI Specification — solves this problem by letting you describe your API once and get interactive documentation, client generation, and contract testing for free. This guide walks you through everything you need to go from a raw Express or Laravel API to a fully documented, testable interface that your team and external consumers will actually love.
OpenAPI Specification (OAS) is the vendor-neutral, open standard that describes REST APIs. Swagger is the original name (donated to the Linux Foundation in 2016) and now refers to the tooling ecosystem — Swagger UI, Swagger Editor, and Swagger Codegen — that works with OAS documents. When developers say "Swagger docs" they usually mean an OpenAPI 3.x spec rendered by Swagger UI.
Understanding the OpenAPI Specification
An OpenAPI document is a JSON or YAML file that describes your API's endpoints, request parameters, response shapes, authentication schemes, and more. OpenAPI 3.1 is the current version (fully aligned with JSON Schema draft 2020-12) but 3.0.x remains the most widely supported version across tooling, so that's what we'll use throughout this guide.
A minimal but complete OpenAPI 3.0 document looks like this:
openapi: 3.0.3
info:
title: User Management API
description: CRUD operations for managing users
version: 1.0.0
contact:
name: Mayur Dabhi
email: mayurdabhi.6@gmail.com
servers:
- url: https://api.example.com/v1
description: Production
- url: http://localhost:3000/v1
description: Local development
paths:
/users:
get:
summary: List all users
operationId: listUsers
tags: [Users]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Paginated list of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create a user
operationId: createUser
tags: [Users]
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'422':
$ref: '#/components/responses/ValidationError'
components:
schemas:
User:
type: object
properties:
id:
type: integer
example: 42
name:
type: string
example: Jane Doe
email:
type: string
format: email
example: jane@example.com
createdAt:
type: string
format: date-time
CreateUserRequest:
type: object
required: [name, email, password]
properties:
name:
type: string
minLength: 2
maxLength: 100
email:
type: string
format: email
password:
type: string
minLength: 8
UserList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
meta:
type: object
properties:
total: { type: integer }
page: { type: integer }
limit: { type: integer }
responses:
Unauthorized:
description: Missing or invalid token
content:
application/json:
schema:
type: object
properties:
message: { type: string, example: Unauthenticated }
ValidationError:
description: Validation failed
content:
application/json:
schema:
type: object
properties:
message: { type: string }
errors:
type: object
additionalProperties:
type: array
items: { type: string }
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
The three most important structural sections are paths (your endpoints), components (reusable schemas, responses, and security schemes), and info (metadata). Using $ref pointers to components keeps the spec DRY — a schema defined once can be referenced across dozens of endpoints.
Swagger UI with Node.js and Express
The fastest way to serve interactive Swagger docs from a Node.js/Express app is the swagger-ui-express package paired with swagger-jsdoc for annotation-driven spec generation.
Install the packages
Add swagger-ui-express and swagger-jsdoc to your project.
npm install swagger-ui-express swagger-jsdoc
Configure swagger-jsdoc
Create a swagger.js config that tells swagger-jsdoc where your JSDoc annotations live.
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.3',
info: {
title: 'My API',
version: '1.0.0',
description: 'REST API documentation',
},
servers: [
{ url: 'http://localhost:3000', description: 'Development' },
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
},
// Scan all route files for JSDoc annotations
apis: ['./src/routes/*.js', './src/models/*.js'],
};
module.exports = swaggerJsdoc(options);
Mount Swagger UI in Express
Add two lines to your main app.js to serve the interactive docs at /api-docs.
const express = require('express');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const app = express();
app.use(express.json());
// Serve Swagger UI at /api-docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'My API Docs',
swaggerOptions: {
persistAuthorization: true, // keep auth tokens across page refreshes
},
}));
// Export the spec as JSON for CI tooling
app.get('/api-docs.json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(swaggerSpec);
});
// Your routes
app.use('/v1/users', require('./routes/users'));
app.listen(3000, () => console.log('API running on http://localhost:3000'));
module.exports = app;
Annotate your routes with JSDoc
Swagger-jsdoc reads JSDoc comments from your route files to build the spec automatically.
const router = require('express').Router();
const UserController = require('../controllers/UserController');
/**
* @openapi
* /v1/users:
* get:
* summary: List all users
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* responses:
* 200:
* description: Array of users
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/User'
* 401:
* description: Unauthorized
*/
router.get('/', UserController.index);
/**
* @openapi
* /v1/users:
* post:
* summary: Create a new user
* tags: [Users]
* security:
* - BearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateUserRequest'
* responses:
* 201:
* description: User created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/User'
* 422:
* description: Validation error
*/
router.post('/', UserController.store);
module.exports = router;
You can define components/schemas inside a JSDoc comment in any file — typically in a dedicated src/schemas/ folder or alongside your Mongoose/Sequelize models. Use @openapi tag (preferred) or @swagger (legacy) at the top of the comment block.
/**
* @openapi
* components:
* schemas:
* User:
* type: object
* properties:
* id:
* type: integer
* example: 1
* name:
* type: string
* example: Jane Doe
* email:
* type: string
* format: email
* example: jane@example.com
* createdAt:
* type: string
* format: date-time
*
* CreateUserRequest:
* type: object
* required: [name, email, password]
* properties:
* name:
* type: string
* minLength: 2
* maxLength: 100
* email:
* type: string
* format: email
* password:
* type: string
* minLength: 8
* writeOnly: true
*/
// This file exists only to hold schema definitions — no JS exports needed
Swagger in Laravel with L5-Swagger
The most popular Swagger package for Laravel is L5-Swagger, a wrapper around zircote/swagger-php that integrates cleanly with Laravel's service provider system. It reads PHP 8 attributes (or PHPDoc annotations) from your controllers and generates an OpenAPI spec on demand.
# Install L5-Swagger
composer require "darkaonline/l5-swagger"
# Publish config and views
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"
# Generate the spec (run whenever you update annotations)
php artisan l5-swagger:generate
The UI is served at /api/documentation by default. You configure the spec path, annotation scan paths, and auth presets in config/l5-swagger.php.
Annotating Laravel Controllers
L5-Swagger supports both PHP 8 native attributes and the legacy PHPDoc-style annotations. PHP 8 attributes are preferred in modern codebases because IDEs understand them natively:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
#[OA\Tag(name: 'Users', description: 'User management endpoints')]
class UserController extends Controller
{
#[OA\Get(
path: '/api/v1/users',
summary: 'List all users',
tags: ['Users'],
parameters: [
new OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'per_page', in: 'query', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(response: 200, description: 'Paginated users'),
new OA\Response(response: 401, description: 'Unauthenticated'),
]
)]
public function index(Request $request)
{
return User::paginate($request->per_page ?? 15);
}
#[OA\Post(
path: '/api/v1/users',
summary: 'Create a user',
tags: ['Users'],
security: [['sanctum' => []]],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(ref: '#/components/schemas/CreateUserRequest')
),
responses: [
new OA\Response(
response: 201,
description: 'User created',
content: new OA\JsonContent(ref: '#/components/schemas/User')
),
new OA\Response(response: 422, description: 'Validation error'),
]
)]
public function store(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:100',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
return response()->json(User::create($data), 201);
}
}
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
class UserController extends Controller
{
/**
* @OA\Get(
* path="/api/v1/users",
* summary="List all users",
* tags={"Users"},
* @OA\Parameter(
* name="page",
* in="query",
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="Paginated users",
* @OA\JsonContent(ref="#/components/schemas/UserCollection")
* ),
* @OA\Response(response=401, description="Unauthenticated")
* )
*/
public function index()
{
return User::paginate(15);
}
}
The global OpenAPI info block (version, title, servers, security) is typically placed in a dedicated class:
<?php
namespace App\Http\Controllers\Api;
use OpenApi\Attributes as OA;
#[OA\Info(
title: 'My Laravel API',
version: '1.0.0',
description: 'REST API documentation',
contact: new OA\Contact(
name: 'Mayur Dabhi',
email: 'mayurdabhi.6@gmail.com'
)
)]
#[OA\Server(
url: 'https://api.example.com',
description: 'Production server'
)]
#[OA\Server(
url: 'http://localhost:8000',
description: 'Local development'
)]
#[OA\SecurityScheme(
securityScheme: 'sanctum',
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
)]
class OpenApiSpec {}
// This class has no methods — it only carries the attribute-based spec metadata
Swagger UI Request / Response Flow
Understanding how Swagger UI works under the hood helps you debug authorization issues and CORS errors that commonly trip people up in production setups.
How Swagger UI loads the spec and executes live API requests
Swagger UI's "Try it out" feature sends real HTTP requests from the user's browser directly to your API. In production, your API server must return appropriate Access-Control-Allow-Origin headers for the domain where Swagger UI is hosted, otherwise requests will be blocked by the browser's CORS policy. Configure CORS carefully — don't just use * on authenticated endpoints.
Documenting Authentication
Most real-world APIs require authentication. OpenAPI 3.0 supports four security scheme types: HTTP (Bearer/Basic), API key (header, query, or cookie), OAuth 2.0, and OpenID Connect. Here's how to document each pattern:
components:
securitySchemes:
# JWT Bearer token (most common for REST APIs)
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
# API key in header
ApiKeyHeader:
type: apiKey
in: header
name: X-API-Key
# API key in query param (less secure, avoid for sensitive endpoints)
ApiKeyQuery:
type: apiKey
in: query
name: api_key
# OAuth2 Authorization Code flow
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/oauth/authorize
tokenUrl: https://auth.example.com/oauth/token
scopes:
read:users: Read user data
write:users: Create and update users
admin: Full administrative access
# Apply security globally (can be overridden per-endpoint)
security:
- BearerAuth: []
paths:
/public/status:
get:
summary: Health check (no auth required)
security: [] # Override to remove auth for this endpoint
responses:
'200':
description: API is running
/users/{id}:
get:
summary: Get user by ID
security:
- BearerAuth: [] # JWT OR
- ApiKeyHeader: [] # API key (user can use either)
parameters:
- name: id
in: path
required: true
schema:
type: integer
Securing the Swagger UI itself
In production, you should restrict who can access /api-docs. Common patterns include:
// Option 1: Disable entirely in production
if (process.env.NODE_ENV !== 'production') {
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
}
// Option 2: Basic auth protection
const basicAuth = require('express-basic-auth');
app.use('/api-docs',
basicAuth({
users: { [process.env.DOCS_USER]: process.env.DOCS_PASS },
challenge: true,
}),
swaggerUi.serve,
swaggerUi.setup(swaggerSpec)
);
// Option 3: IP allowlist (for internal tools)
app.use('/api-docs', (req, res, next) => {
const ip = req.ip;
const allowed = ['127.0.0.1', '10.0.0.0/8'];
if (!allowed.some(range => ip.startsWith(range.split('/')[0]))) {
return res.status(403).json({ message: 'Forbidden' });
}
next();
}, swaggerUi.serve, swaggerUi.setup(swaggerSpec));
Spec Validation and Contract Testing
Writing a spec is only half the work. The real value comes when you use that spec as a contract that your implementation must satisfy. Several tools can validate that your running API actually matches its OpenAPI document:
| Tool | What it validates | When to use |
|---|---|---|
| openapi-validator (IBM) | Spec linting — checks spec quality and style rules | Pre-commit / CI on the YAML file |
| schemathesis | Property-based testing — generates and fires requests against your API | Integration test suite, fuzzing |
| dredd | Response contract — checks that real API responses match the spec | CI pipeline against staging |
| express-openapi-validator | Runtime request/response validation (middleware) | Development server, catches drift immediately |
| Spectral | Customizable linting with rulesets | Enforcing API design standards |
npm install express-openapi-validator
const OpenApiValidator = require('express-openapi-validator');
const path = require('path');
app.use(
OpenApiValidator.middleware({
apiSpec: path.join(__dirname, 'openapi.yaml'), // or your spec JSON
validateRequests: true, // validate incoming request bodies, params, headers
validateResponses: true, // validate outgoing response bodies (dev only)
})
);
// The validator throws structured errors that you catch with a global handler
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
});
With validateResponses: true enabled during development, any time your code returns a response that doesn't match the spec, you'll get an immediate 500 error with details. This catches spec drift before it ever reaches your consumers.
Swagger Tooling Ecosystem
Swagger UI is just one piece of a broader ecosystem. Once you have an OpenAPI spec, a whole range of tools can consume it:
Code Generation with OpenAPI Generator
OpenAPI Generator can produce type-safe API clients in 50+ languages from your spec. This eliminates hand-written HTTP client code and keeps your SDK in sync with the API automatically:
# Install via Homebrew (macOS/Linux)
brew install openapi-generator
# Generate a TypeScript/Fetch client
openapi-generator-cli generate \
-i https://api.example.com/api-docs.json \
-g typescript-fetch \
-o ./generated/api-client
# Generate a Python client
openapi-generator-cli generate \
-i openapi.yaml \
-g python \
-o ./generated/python-client
# Generate server stubs (Express, Spring, Laravel, etc.)
openapi-generator-cli generate \
-i openapi.yaml \
-g nodejs-express-server \
-o ./generated/server
The generated TypeScript client includes type definitions for every schema, making it impossible to call an endpoint with the wrong parameters.
Postman Import from OpenAPI
Postman can import your OpenAPI spec and create a complete collection with pre-built requests for every endpoint. This is useful for QA teams who prefer Postman's environment and test runner features.
- Open Postman → Import
- Paste your spec URL (e.g.,
http://localhost:3000/api-docs.json) or upload the YAML file - Postman generates a collection — all endpoints, example request bodies, and response schemas are pre-populated
- Add environment variables for
baseUrlandtoken, then run the collection against any environment
Choosing a Documentation UI
Swagger UI is the most popular but not the only option. Here's how the main alternatives compare:
| Tool | Strengths | Weaknesses | Best for |
|---|---|---|---|
| Swagger UI | Industry standard, "Try it out" built-in, huge ecosystem | Plain default design, large bundle | Internal tooling, developer portals |
| Redoc | Beautiful three-panel layout, fast rendering, React-friendly | No "Try it out" in free tier | Public-facing API docs |
| Scalar | Modern UI, dark mode, client code snippets, free | Newer, smaller community | Modern developer portals |
| Stoplight Elements | Beautiful design, embeddable web component | Full platform requires paid plan | Product documentation sites |
| Postman | Full API lifecycle, team collaboration | Not embeddable in your app, requires account | API development workflow |
Best Practices for Great API Docs
A technically valid spec is not the same as good documentation. These practices make the difference between docs that developers tolerate and docs they actually enjoy using:
API Documentation Best Practices
- Write meaningful summaries and descriptions.
summary: Get useris useless.summary: Retrieve a single user by their numeric ID. Returns 404 if the user doesn't exist.is helpful. - Always include examples. Every schema property should have an
examplevalue. Swagger UI shows these in the "Example Value" panel and uses them for "Try it out" pre-fills. - Document all error responses. 400, 401, 403, 404, 422, 429, and 500 — consumers need to know what to expect and handle. Reuse error schemas via
$ref. - Use tags to group endpoints logically. Tags become the navigation headers in Swagger UI. Group by resource (Users, Orders, Payments), not by HTTP method.
- Version your spec. Keep the
info.versionin sync with your API version (use SemVer). Store the spec in version control alongside your code. - Generate from code, not alongside it. Maintaining a separate YAML file by hand leads to drift. Use annotations or schema reflection so the spec is always derived from the running code.
- Add
operationIdto every endpoint. These become function names in generated clients. Use camelCase and be descriptive:getUserById, notgetUser1. - Mark deprecated endpoints. Add
deprecated: trueto endpoints you're phasing out. Swagger UI renders them with a strikethrough, alerting consumers proactively.
Add a CI step that generates your spec and runs openapi-validator on every pull request. If a developer's code changes break the spec (missing required property, wrong type), the build fails before merge. This is far cheaper than discovering spec drift after you've shipped a breaking change to API consumers.
name: Validate API Spec
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Generate OpenAPI spec
run: node src/generate-spec.js > openapi.json
- name: Validate spec with IBM validator
run: |
npm install -g ibm-openapi-validator
lint-openapi openapi.json --errors-only
- name: Run contract tests with Schemathesis
run: |
pip install schemathesis
# Start API in background
node src/app.js &
sleep 3
# Fuzz all endpoints for spec compliance
schemathesis run openapi.json --base-url http://localhost:3000 --checks all
API documentation with Swagger is no longer optional for teams that care about developer experience. An OpenAPI spec gives you interactive docs, client generation, contract testing, and a shared language between backend and frontend teams — all from a single source of truth. Start with a minimal YAML file describing your most important endpoints, wire up Swagger UI so the team can see it immediately, and grow the spec incrementally as your API evolves. The small investment in annotation discipline pays back every time a new developer onboards or a consumer asks "how do I use this endpoint?"