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
URL path versioning (recommended)
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
File Upload to S3/R2
Presigned URLs, multipart upload, progress tracking, and file validation
GraphQL API with Apollo Server
Schema, resolvers, context, dataloaders, and error handling
Background Jobs with BullMQ
Queue setup, workers, retries, job scheduling, and monitoring dashboard