TrueSpec

API Versioning Patterns

URL versioning, header versioning, and migration guides for clients

What You’ll Build

After following this guide, you will have a working implementation of api versioning patterns in your project. Version your API so you can make breaking changes without breaking existing clients. Covers the three main approaches: URL path versioning (/api/v1/), header versioning (Accept header), and query parameter versioning. Includes practical migration patterns.

Use Cases & Problems Solved

  • Build reliable server endpoints that clients can consume consistently
  • Handle common backend patterns like routing, middleware, and error handling
  • Provide a clean interface between your frontend and data layer

Prerequisites

  • Express.js or any web framework
  • Existing API with clients

Step-by-Step Implementation

The following snippet shows how to url path versioning (recommended). Copy this into your project and adjust the values for your environment.

// routes/v1/users.js — original API shape
router.get('/api/v1/users', (req, res) => {
  // Returns: { name: 'John Doe' }
  res.json(users.map(u => ({ name: u.fullName })));
});

// routes/v2/users.js — new API shape (breaking change)
router.get('/api/v2/users', (req, res) => {
  // Returns: { firstName: 'John', lastName: 'Doe' }
  res.json(users.map(u => ({ firstName: u.first, lastName: u.last })));
});

// Mount both — old clients keep working
app.use(v1Router);
app.use(v2Router);

Deprecation headers for sunset notices

The following snippet shows how to deprecation headers for sunset notices. Copy this into your project and adjust the values for your environment.

// Warn v1 clients that the API will be removed
app.use('/api/v1', (req, res, next) => {
  res.set('Deprecation', 'true');
  res.set('Sunset', 'Sat, 01 Jun 2026 00:00:00 GMT');
  res.set('Link', '</api/v2>; rel="successor-version"');
  next();
});

⚠️ Don’t Do This

❌ Making breaking changes to an existing API without versioning

// Renamed field — ALL existing clients break immediately!
// Before: { name: 'John Doe' }
// After:  { firstName: 'John', lastName: 'Doe' }
router.get('/api/users', (req, res) => {
  res.json(users.map(u => ({ firstName: u.first, lastName: u.last })));
});

✅ Add new version, keep old version running, set sunset date

// Old API still works for existing clients
app.use('/api/v1', v1Router);
// New API for new/upgraded clients
app.use('/api/v2', v2Router);

Testing

Add these tests to verify your API endpoints work correctly:

// __tests__/api.test.ts
import { describe, it, expect } from 'vitest';

describe('API Versioning Patterns', () => {
  it('should return 200 for valid requests', async () => {
    const res = await fetch('/api/endpoint', { method: 'GET' });
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data).toBeDefined();
  });

  it('should return 400 for invalid input', async () => {
    const res = await fetch('/api/endpoint', {
      method: 'POST',
      body: JSON.stringify({}),
    });
    expect(res.status).toBe(400);
  });

  it('should handle errors gracefully', async () => {
    const res = await fetch('/api/endpoint/nonexistent');
    expect(res.status).toBe(404);
  });
});

Verification

# Both versions should work simultaneously:
curl http://localhost:3000/api/v1/users
# Returns old format + Deprecation header

curl http://localhost:3000/api/v2/users
# Returns new format

Related Specs

Intermediate

File Upload to S3/R2

Presigned URLs, multipart upload, progress tracking, and file validation

API & Backend
Intermediate

GraphQL API with Apollo Server

Schema, resolvers, context, dataloaders, and error handling

API & Backend
Intermediate

Background Jobs with BullMQ

Queue setup, workers, retries, job scheduling, and monitoring dashboard

API & Backend