GET POST PUT
Backend

API Documentation with Swagger

Mayur Dabhi
Mayur Dabhi
May 15, 2026
14 min read

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.

Swagger vs OpenAPI — the terminology

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.yaml
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.

1

Install the packages

Add swagger-ui-express and swagger-jsdoc to your project.

Terminal
npm install swagger-ui-express swagger-jsdoc
2

Configure swagger-jsdoc

Create a swagger.js config that tells swagger-jsdoc where your JSDoc annotations live.

src/swagger.js
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);
3

Mount Swagger UI in Express

Add two lines to your main app.js to serve the interactive docs at /api-docs.

src/app.js
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;
4

Annotate your routes with JSDoc

Swagger-jsdoc reads JSDoc comments from your route files to build the spec automatically.

src/routes/users.js
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;
Defining reusable schemas in JSDoc

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.

src/schemas/user.schema.js
/**
 * @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.

Terminal
# 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.

Browser Swagger UI localhost:3000 1. GET /api-docs.json API Server Express / Laravel :3000 / :8000 2. OpenAPI JSON Renders UI Endpoints, forms, auth controls parses 3. Try It Out → live HTTP request 4. JSON response Database MySQL / PostgreSQL Redis / MongoDB ⚠ CORS Swagger UI sends requests directly from the browser. Your API must allow CORS.

How Swagger UI loads the spec and executes live API requests

CORS in production

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:

Authentication Schemes in OpenAPI YAML
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:

Restricting Swagger UI in Express
// 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
Runtime validation with express-openapi-validator
npm install express-openapi-validator
src/app.js — add request/response validation middleware
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:

Terminal
# 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.

  1. Open Postman → Import
  2. Paste your spec URL (e.g., http://localhost:3000/api-docs.json) or upload the YAML file
  3. Postman generates a collection — all endpoints, example request bodies, and response schemas are pre-populated
  4. Add environment variables for baseUrl and token, 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 user is 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 example value. 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.version in 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 operationId to every endpoint. These become function names in generated clients. Use camelCase and be descriptive: getUserById, not getUser1.
  • Mark deprecated endpoints. Add deprecated: true to endpoints you're phasing out. Swagger UI renders them with a strikethrough, alerting consumers proactively.
Automate spec generation in CI

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.

.github/workflows/api-docs.yml
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?"

Swagger OpenAPI API Documentation REST API Node.js Laravel Backend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.