Skip to content
intermediate Phase 11 · API Design

API Documentation & Versioning

Document APIs with OpenAPI/Swagger, implement versioning strategies, and manage breaking changes.

1h
0 problems
Topic Progress 0%

Versioning Strategies and Selection

Versioning Strategies and Selection

URL Path Versioning

The most widely adopted strategy for public APIs. The version number lives in the URL, making it explicit, cacheable, and easy to route:

// Express router with versioned mounts
const v1Router = express.Router();
const v2Router = express.Router();

// v1: flat response format
v1Router.get('/users', async (req, res) => {
  const users = await User.findAll();
  res.json({ users, total: users.length });
});

// v2: envelope with pagination metadata
v2Router.get('/users', async (req, res) => {
  const { page = 1, limit = 20 } = req.query;
  const offset = (page - 1) * limit;
  const [users, total] = await Promise.all([
    User.findAll({ limit, offset, order: [['createdAt', 'DESC']] }),
    User.count()
  ]);
  res.json({
    data: users.map(u => ({ id: u.id, name: u.name, email: u.email })),
    meta: { page, limit, total, totalPages: Math.ceil(total / limit) }
  });
});

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

Header Versioning

Clean URLs with version info sent via the Accept header. Useful for internal APIs where you control all clients:

// Custom vendor media type: Accept: application/vnd.myapi.v2+json
app.use('/api', (req, res, next) => {
  const accept = req.headers.accept || '';
  const match = accept.match(/application\/vnd\.myapi\.v(\d+)\+json/);
  req.apiVersion = match ? parseInt(match[1]) : 1;
  next();
});

app.get('/api/users', (req, res) => {
  if (req.apiVersion === 1) return res.json(usersV1Format);
  if (req.apiVersion === 2) return res.json(usersV2Format);
  res.status(406).json({ error: 'Unsupported API version. Use Accept: application/vnd.myapi.v2+json' });
});

Query Parameter Versioning

Simplest to implement but pollutes the URL and complicates caching:

app.get('/api/users', (req, res) => {
  const version = parseInt(req.query.version) || 1;
  if (version === 1) return res.json(formatV1(users));
  if (version === 2) return res.json(formatV2(users));
  res.status(400).json({ error: 'Invalid version parameter' });
});

Strategy Comparison

Strategy Cacheable Explicit Browser-Testable RESTful
URL path Yes (CDN-friendly) Yes Yes Purists oppose
Header Yes (Vary header) No (hidden) Harder Yes
Query param Yes Yes Yes Partially

Rule of thumb: Use URL path versioning for public APIs consumed by third parties. Use header versioning for internal APIs where you control all clients and want clean resource URIs.

Breaking vs Non-Breaking Changes and Deprecation

Breaking vs Non-Breaking Changes and Deprecation

Non-Breaking Changes (Safe to Add)

These do not require a new version — existing clients continue to work:

// Adding a new optional field to a response
// v1 response still works, new field is simply appended
{
  "id": "usr_123",
  "name": "Alice",
  "email": "alice@example.com",
  "avatarUrl": null,   // NEW FIELD — clients ignore unknown fields
  "createdAt": "2026-01-15T10:30:00Z"
}

// Adding a new optional query parameter
// Existing: GET /api/v1/users?status=active
// Updated: GET /api/v1/users?status=active&sort=createdAt
// Old clients still work; new clients can use the sort param

// Adding a new endpoint under the same version
app.post('/api/v1/users/:id/avatar', uploadAvatar);  // NEW — no impact

Breaking Changes (Require New Version)

// 1. Renaming or removing a field
// v1: { name: "Alice" }
// v2: { profile: { firstName: "Alice" } }  // BREAKING

// 2. Changing a field type
// v1: { id: 123 }       (number)
// v2: { id: "usr_123" }  (string)  // BREAKING

// 3. Changing authentication requirements
// v1: public endpoints
// v2: all endpoints require Bearer token  // BREAKING

// 4. Narrowing allowed values
// v1: status accepts "active", "inactive"
// v2: status only accepts "active"  // BREAKING for inactive users

// 5. Changing error response structure
// v1: { error: "Not found" }
// v2: { error: { code: "NOT_FOUND", message: "Resource not found" } }  // BREAKING

Deprecation Workflow

Use HTTP headers to communicate deprecation timelines to consumers:

// Step 1: Mark endpoint as deprecated (still fully functional)
router.get('/users', (req, res) => {
  res.set('Deprecation', 'true');
  res.set('Sunset', 'Sat, 01 Mar 2027 00:00:00 GMT');
  res.set('Link', '</api/v2/users>; rel="successor-version"');
  res.json(formatV1(users));
});

// Step 2: Log usage of deprecated endpoints to track migration
router.get('/users', (req, res) => {
  deprecationLogger.record({ endpoint: '/api/v1/users', client: req.headers['user-agent'] });
  res.set('Deprecation', 'true');
  res.set('Sunset', 'Sat, 01 Mar 2027 00:00:00 GMT');
  res.json(formatV1(users));
});

// Step 3: After sunset date, return 410 Gone
router.get('/users', (req, res) => {
  res.status(410).json({
    error: {
      code: 'ENDPOINT_DEPRECATED',
      message: 'This endpoint has been removed. Migrate to /api/v2/users.',
      migrationGuide: 'https://docs.api.com/migration/v1-to-v2'
    }
  });
});

Changelog Format

Maintain a structured changelog so API consumers can track changes:

# API Changelog

## [2.0.0] - 2026-03-01
### Breaking Changes
- Changed user response: `name` field split into `firstName` and `lastName`
- Authentication required on all endpoints (previously public)

### Migration Guide
- See https://docs.api.com/migration/v1-to-v2
- v1 endpoints remain available until 2027-03-01

## [1.2.0] - 2026-01-15
### Added
- `avatarUrl` field on User resource
- `sort` query parameter on GET /users

### Deprecated
- None

OpenAPI Documentation for Versioned APIs

OpenAPI Documentation for Versioned APIs

Multi-Version OpenAPI Specification

Document both versions side by side so consumers understand the differences:

# openapi.yaml
openapi: 3.0.3
info:
  title: E-Commerce API
  version: 2.0.0
  description: |
    ## API Versioning
    - **v1** (deprecated): Original release. Sunset date: 2027-03-01
    - **v2** (current): New response format with envelope pattern
    
    All new integrations must use v2. Migration guide: https://docs.api.com/migration/v1-to-v2
  contact:
    name: API Support
    email: api-support@example.com

servers:
  - url: https://api.example.com/api/v1
    description: v1 (deprecated)
  - url: https://api.example.com/api/v2
    description: v2 (current)

paths:
  /users:
    get:
      operationId: listUsers
      summary: List all users
      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 user list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'

components:
  schemas:
    UserListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/User'
        meta:
          $ref: '#/components/schemas/PaginationMeta'

    User:
      type: object
      required: [id, name, email]
      properties:
        id:
          type: string
          example: usr_abc123
        name:
          type: string
          example: Alice Johnson
        email:
          type: string
          format: email
        avatarUrl:
          type: string
          format: uri
          nullable: true
        createdAt:
          type: string
          format: date-time

    PaginationMeta:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer

    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: VALIDATION_ERROR
            message:
              type: string
              example: Invalid request parameters
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  message:
                    type: string

Automated Version Diff in CI

Generate a diff report on every PR to catch accidental breaking changes:

# .github/workflows/api-diff.yml
name: API Breaking Changes
on:
  pull_request:
    paths: ['openapi.yaml']

jobs:
  breaking-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install openapi-diff
        run: npm install -g openapi-diff
      - name: Compare with main branch
        run: |
          git fetch origin main
          git show origin/main:openapi.yaml > /tmp/openapi-main.yaml
          openapi-diff /tmp/openapi-main.yaml openapi.yaml \
            --fail-on-incompatible \
            --output /tmp/api-diff-report.md
      - name: Comment PR with diff
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const diff = fs.readFileSync('/tmp/api-diff-report.md', 'utf8');
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `## API Breaking Changes Report\n\n${diff}`
            });

Version Header Middleware

Always include version metadata in responses so consumers can debug:

app.use((req, res, next) => {
  const originalJson = res.json.bind(res);
  res.json = (body) => {
    res.set('X-API-Version', req.apiVersion || 'v2');
    res.set('X-API-Deprecated', req.isDeprecated ? 'true' : 'false');
    if (req.sunsetDate) res.set('Sunset', req.sunsetDate);
    return originalJson(body);
  };
  next();
});

OpenAPI Validation Middleware

Enforce the spec at runtime to catch drift between docs and implementation:

import swaggerValidation from 'express-openapi-validator';

app.use(
  swaggerValidation.middleware({
    apiSpec: './openapi.yaml
    validateRequests: true,
    validateResponses: process.env.NODE_ENV !== 'production',
    ignorePaths: /^\/api\/v1\//,  // Skip validation for deprecated v1
  })
);

Quiz

1. Your public API is consumed by 50 third-party mobile apps. You need to change the user response format. Which versioning strategy should you use?

Question 1 options

2. You add an optional `phone` field to the User response in v1. Do you need a v2 release?

Question 2 options

3. What HTTP headers should you include when deprecating an API endpoint?

Question 3 options

Flashcards

Question

What is the difference between a breaking and non-breaking API change?

Answer

A breaking change alters the contract in a way that causes existing clients to fail — removing a field, renaming a property, changing a field's type, narrowing allowed values, or adding mandatory authentication. A non-breaking change is additive: new optional fields, new optional query parameters, or new endpoints. Non-breaking changes can ship in the current version; breaking changes require a new version with a deprecation period.

Question

What does the HTTP Sunset header do?

Answer

The Sunset header (RFC 8594) tells clients when an endpoint or API version will be removed. It contains an HTTP-date value (e.g., Sunset: Sat, 01 Mar 2027 00:00:00 GMT). Clients can use this to automate migration alerts, build countdown timers, or trigger automated upgrades before the deadline. After the sunset date, the server typically returns 410 Gone.

Question

Why is URL path versioning preferred for public APIs?

Answer

URL path versioning (/api/v1/users) is explicit, easily cacheable by CDNs (each version is a distinct URL), simple to test in a browser, and universally understood by API consumers. It requires no special client configuration — any HTTP client can target a version by changing the URL. The main criticism (REST purists say URLs should identify resources, not versions) is outweighed by practical benefits for third-party consumption.

Revision Notes

Key Takeaways

  • 1. URL path versioning is the safest choice for public APIs — it is explicit, cacheable, and easy to debug
  • 2. Adding optional fields, optional query parameters, and new endpoints are non-breaking changes that do not require a new version
  • 3. Removing fields, renaming properties, changing types, and narrowing allowed values are breaking changes that require a new version
  • 4. Use Deprecation, Sunset, and Link headers to give clients a machine-readable deprecation timeline
  • 5. OpenAPI specs should document all active versions and mark deprecated endpoints with the deprecated: true flag
  • 6. Maintain a structured changelog so consumers can track what changed in each release
  • 7. Always include X-API-Version and X-API-Deprecated headers in responses for debugging

Interview Tips

  • Explain the three versioning strategies and when you would choose each one
  • Give examples of breaking vs non-breaking changes and how you decide when to bump a version
  • Describe a deprecation workflow: how you communicate, track usage, and enforce sunset dates
  • Explain how you would structure an OpenAPI spec to document multiple API versions
  • Discuss how to detect accidental breaking changes in CI using OpenAPI diff tools

Cheat Sheet

API Versioning Cheat Sheet

Versioning Strategies:

  • URL path: /api/v1/users — best for public APIs
  • Header: Accept: application/vnd.api.v2+json — clean URLs, internal APIs
  • Query param: ?version=2 — simplest but pollutes URLs

Breaking Changes (require new version):

  • Remove or rename a field
  • Change field type (number → string)
  • Narrow allowed values
  • Add required authentication
  • Change error response structure

Non-Breaking Changes (safe in current version):

  • Add optional field to response
  • Add optional query parameter
  • Add new endpoint

Deprecation Headers:

  • Deprecation: true
  • Sunset: <HTTP-date>
  • Link: </api/v2/users>; rel="successor-version"

OpenAPI Best Practices:

  • Document all versions in the same spec or separate files per version
  • Use deprecated: true on removed endpoints
  • Run openapi-diff in CI to catch breaking changes on PRs
  • Use express-openapi-validator to enforce the spec at runtime

After Sunset:

  • Return 410 Gone with a migration guide link
  • Log remaining hits to identify slow adopters
  • Keep the endpoint stub for at least 30 days after sunset