OpenAPI Specification: Documenting REST APIs

OpenAPI Specification (OAS) is the industry standard for describing REST APIs in a machine-readable format. Originally developed as Swagger Specification by SmartBear Software, it was donated to the...

Key Insights

  • OpenAPI Specification transforms API development by enabling automatic documentation generation, client SDK creation, and runtime validation—eliminating the documentation drift that plagues most REST APIs
  • The code-first approach using tools like swagger-jsdoc keeps your OpenAPI spec synchronized with implementation by extracting documentation directly from JSDoc comments in your Express routes
  • Integrating OpenAPI validation middleware catches contract violations at runtime, while generating TypeScript types from your spec ensures compile-time safety across your entire stack

Introduction to OpenAPI Specification

OpenAPI Specification (OAS) is the industry standard for describing REST APIs in a machine-readable format. Originally developed as Swagger Specification by SmartBear Software, it was donated to the OpenAPI Initiative in 2016 and renamed to OpenAPI Specification 3.0.

The value proposition is simple: write your API contract once, and generate documentation, client libraries, server stubs, and validation logic automatically. This eliminates the chronic problem of outdated documentation that every API team faces. When your documentation is generated from the same source of truth as your implementation, they can’t drift apart.

Here’s a minimal OpenAPI 3.0 document structure:

openapi: 3.0.0
info:
  title: Task Management API
  version: 1.0.0
  description: API for managing tasks and projects
servers:
  - url: https://api.example.com/v1
    description: Production server
  - url: http://localhost:3000/v1
    description: Development server
paths:
  /tasks:
    get:
      summary: List all tasks
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object

This YAML document is both human-readable and machine-parseable, making it ideal for collaboration between teams and tools.

Core OpenAPI Components

An OpenAPI document consists of several key sections. The info object contains metadata about your API. The servers array defines base URLs for different environments. The paths object is where you define your endpoints, and components holds reusable schemas, parameters, and responses.

Each path can have multiple operations (GET, POST, PUT, DELETE, etc.). Each operation describes parameters, request bodies, and possible responses. Here’s a complete endpoint definition:

paths:
  /tasks:
    post:
      summary: Create a new task
      tags:
        - Tasks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
                - priority
              properties:
                title:
                  type: string
                  minLength: 1
                  maxLength: 200
                description:
                  type: string
                priority:
                  type: string
                  enum: [low, medium, high]
                dueDate:
                  type: string
                  format: date-time
      responses:
        '201':
          description: Task created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized

Notice the $ref syntax—this references reusable schema definitions we’ll define in the components section.

Creating OpenAPI Specs for Express.js APIs

You have two approaches: write YAML/JSON manually or generate it from code. Manual YAML works for design-first workflows, but maintaining it alongside evolving code is painful. The code-first approach using swagger-jsdoc is more maintainable.

Install the required packages:

npm install swagger-jsdoc swagger-ui-express

Configure swagger-jsdoc to scan your route files:

// swagger.js
const swaggerJsdoc = require('swagger-jsdoc');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'Task Management API',
      version: '1.0.0',
      description: 'API for managing tasks and projects',
    },
    servers: [
      {
        url: 'http://localhost:3000/v1',
        description: 'Development server',
      },
    ],
  },
  apis: ['./routes/*.js'], // Path to route files with JSDoc comments
};

module.exports = swaggerJsdoc(options);

Now document your Express routes with JSDoc comments:

// routes/tasks.js
const express = require('express');
const router = express.Router();

/**
 * @openapi
 * /tasks:
 *   post:
 *     summary: Create a new task
 *     tags:
 *       - Tasks
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - title
 *               - priority
 *             properties:
 *               title:
 *                 type: string
 *               priority:
 *                 type: string
 *                 enum: [low, medium, high]
 *     responses:
 *       201:
 *         description: Task created successfully
 *       400:
 *         description: Invalid request body
 */
router.post('/tasks', async (req, res) => {
  // Implementation
  const task = await createTask(req.body);
  res.status(201).json(task);
});

module.exports = router;

This approach keeps documentation next to implementation, making it easier to keep them synchronized.

Advanced Schema Definitions

Reusable components eliminate duplication and make your spec more maintainable. Define common schemas once and reference them throughout your document:

components:
  schemas:
    Task:
      type: object
      required:
        - id
        - title
        - priority
        - status
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
        priority:
          type: string
          enum: [low, medium, high]
        status:
          type: string
          enum: [todo, in_progress, done]
        createdAt:
          type: string
          format: date-time
    
    Error:
      type: object
      properties:
        message:
          type: string
        errors:
          type: array
          items:
            type: object
  
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key

security:
  - bearerAuth: []

For complex validation scenarios, use oneOf, anyOf, or allOf:

TaskUpdate:
  oneOf:
    - $ref: '#/components/schemas/PriorityUpdate'
    - $ref: '#/components/schemas/StatusUpdate'
  discriminator:
    propertyName: updateType

Generating Interactive Documentation

Swagger UI provides a beautiful, interactive interface for your API documentation. Users can read documentation and test endpoints directly in the browser.

Set it up in your Express application:

// app.js
const express = require('express');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');

const app = express();

app.use(express.json());

// Serve OpenAPI spec as JSON
app.get('/api-docs.json', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.send(swaggerSpec);
});

// Serve Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
  customCss: '.swagger-ui .topbar { display: none }',
  customSiteTitle: 'Task API Documentation',
}));

// Your routes
app.use('/v1', require('./routes/tasks'));

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
  console.log('API docs available at http://localhost:3000/api-docs');
});

Now navigate to http://localhost:3000/api-docs to see your interactive documentation.

Validation and Code Generation

Runtime validation ensures requests and responses match your OpenAPI contract. Use express-openapi-validator:

npm install express-openapi-validator

Add validation middleware:

const OpenApiValidator = require('express-openapi-validator');

app.use(
  OpenApiValidator.middleware({
    apiSpec: './openapi.yaml',
    validateRequests: true,
    validateResponses: true,
  })
);

// Error handler for validation errors
app.use((err, req, res, next) => {
  if (err.status === 400) {
    res.status(400).json({
      message: 'Validation error',
      errors: err.errors,
    });
  } else {
    next(err);
  }
});

Generate TypeScript types from your OpenAPI spec:

npm install -D openapi-typescript
npx openapi-typescript ./openapi.yaml -o ./types/api.ts

This creates type-safe interfaces for your entire API:

import type { paths } from './types/api';

type TaskCreateRequest = paths['/tasks']['post']['requestBody']['content']['application/json'];
type TaskResponse = paths['/tasks']['post']['responses']['201']['content']['application/json'];

Best Practices and Tooling

Version your API in the URL path (/v1/tasks) and maintain separate OpenAPI specs for each major version. Use semantic versioning for your spec’s info.version field.

Lint your OpenAPI specs with Spectral to catch errors and enforce consistency:

npm install -D @stoplight/spectral-cli

Create a .spectral.yaml configuration:

extends: [[spectral:oas, all]]
rules:
  operation-description: error
  operation-tags: error

Add validation scripts to package.json:

{
  "scripts": {
    "openapi:validate": "spectral lint openapi.yaml",
    "openapi:generate-types": "openapi-typescript openapi.yaml -o types/api.ts",
    "openapi:generate-client": "openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ./client",
    "precommit": "npm run openapi:validate"
  }
}

Integrate these scripts into your CI/CD pipeline to catch spec violations before they reach production. Use Git hooks to validate specs on commit.

The key to success with OpenAPI is treating your spec as a first-class artifact. Keep it in version control, validate it automatically, and generate everything you can from it. When your documentation, validation logic, and client code all derive from the same source, consistency becomes automatic rather than aspirational.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.