API versioning is one of those topics developers avoid until a breaking change forces the issue. Good versioning strategy lets you evolve your API without breaking existing integrations — and Claude Code helps implement it correctly: version routing, deprecation headers, backward-compatible expansion, and the patterns that delay needing major version bumps.
This guide covers API versioning with Claude Code: versioning strategies, backward-compatible evolution, deprecation workflows, and multi-version maintenance.
Versioning Strategy Choice
Should I version my API via URL (/v1/users) or via Accept headers?
What are the tradeoffs?
URL versioning (/v1/users, /v2/users):
- Easy to test in browser/curl
- Cache-friendly (different URL = different cached resource)
- Clear from logs what version clients use
- Con: “ugly” URLs; technically violates REST (same resource, different URL)
Header versioning (Accept: application/vnd.myapi.v2+json):
- Semantically cleaner REST
- Same URL for same resource
- Con: harder to test/debug; caching requires
Vary: Acceptheader
Recommendation: URL versioning for public APIs or APIs with external integrations. Header versioning when you control all clients.
URL Versioning Implementation
Implement URL versioning for a Node.js Express API.
All routes should work under /v1. New /v2 routes can
override specific endpoints.
// src/versioning/router.ts
import { Router, Request, Response, NextFunction } from 'express';
export function createVersionedRouter() {
const router = Router();
// Mount v1 routes
const v1Router = require('./v1').default;
router.use('/v1', v1Router);
// Mount v2 routes (inherits unmodified v1 routes)
const v2Router = createV2Router(v1Router);
router.use('/v2', v2Router);
return router;
}
function createV2Router(v1Router: Router): Router {
const v2Router = Router();
// v2 overrides specific endpoints
const v2UsersRouter = require('./v2/users').default;
v2Router.use('/users', v2UsersRouter);
// All other routes fall back to v1
v2Router.use('/', v1Router);
return v2Router;
}
// src/versioning/v1/users.ts
import { Router } from 'express';
const router = Router();
router.get('/', async (req, res) => {
const users = await db.users.findAll();
// v1 response format: array directly
res.json(users.map(u => ({
id: u.id,
name: u.name,
email: u.email,
})));
});
export default router;
// src/versioning/v2/users.ts — Breaking change: wrapped response + pagination
import { Router } from 'express';
const router = Router();
router.get('/', async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const users = await db.users.findPaginated({ page, perPage: 20 });
// v2 response format: wrapped with metadata
res.json({
data: users.items.map(u => ({
id: u.id,
fullName: u.name, // Changed: name → fullName
email: u.email,
createdAt: u.createdAt, // Added: new field
})),
pagination: {
page: users.page,
perPage: users.perPage,
total: users.total,
},
});
});
export default router;
Deprecation Headers and Sunset
Implement deprecation warnings when clients call old API versions.
Show them when it's being removed and where to find the replacement.
// src/middleware/deprecation.ts
import { Request, Response, NextFunction } from 'express';
interface DeprecationConfig {
sunsetDate: string; // ISO 8601
deprecatedSince: string;
link: string; // Migration guide URL
}
const versionDeprecations: Record<string, DeprecationConfig> = {
'v1': {
sunsetDate: '2026-12-31T23:59:59Z',
deprecatedSince: '2026-06-01',
link: 'https://docs.example.com/api/migration/v1-to-v2',
},
};
export function deprecationMiddleware(version: string) {
return (req: Request, res: Response, next: NextFunction) => {
const config = versionDeprecations[version];
if (!config) return next();
// RFC 8594 Deprecation header
res.setHeader('Deprecation', `date="${config.deprecatedSince}"`);
res.setHeader('Sunset', config.sunsetDate);
res.setHeader('Link', `<${config.link}>; rel="deprecation", <${config.link}>; rel="successor-version"`);
// Log deprecated API usage for tracking
console.warn(`Deprecated API call: ${version} ${req.method} ${req.path} - Client: ${req.headers['user-agent']}`);
next();
};
}
// Apply to v1 routes:
router.use('/v1', deprecationMiddleware('v1'), v1Router);
Backward-Compatible Evolution
I need to rename the 'fullName' field to separate 'firstName'/'lastName'.
How do I do this without a major version bump?
// Backward-compatible field expansion: add new, keep old
// v1 → v2-compatible approach (no major version needed)
// Step 1: Accept both old and new format in input
interface CreateUserInput {
// New fields
firstName?: string;
lastName?: string;
// Old field — still accepted, mapped to new fields
fullName?: string; // @deprecated — use firstName + lastName
}
function normalizeUserInput(raw: CreateUserInput) {
if (raw.fullName && !raw.firstName && !raw.lastName) {
const parts = raw.fullName.split(' ');
return {
firstName: parts[0],
lastName: parts.slice(1).join(' ') || '',
};
}
return { firstName: raw.firstName ?? '', lastName: raw.lastName ?? '' };
}
// Step 2: Return both in output during transition period
interface UserResponse {
id: string;
firstName: string;
lastName: string;
fullName: string; // Computed for backward compat — remove in v3
email: string;
}
function serializeUser(user: User): UserResponse {
return {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
fullName: `${user.firstName} ${user.lastName}`, // Keep for backward compat
email: user.email,
};
}
Breaking Change Detection in CI
# .github/workflows/api-breaking-changes.yml
name: Detect API Breaking Changes
on:
pull_request:
paths:
- 'src/routes/**'
- 'api/openapi.yaml'
jobs:
detect-breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Compare OpenAPI spec against main branch
- name: Install oasdiff
run: npm install -g @oasdiff/oasdiff
- name: Check for breaking changes
run: |
git show origin/main:api/openapi.yaml > /tmp/api-main.yaml
# oasdiff outputs breaking changes and exits 1 if any found
oasdiff breaking /tmp/api-main.yaml api/openapi.yaml \
--fail-on ERR 2>&1 | tee breaking-changes.txt
if [ -s breaking-changes.txt ]; then
echo "## ⚠️ Breaking API Changes Detected" >> $GITHUB_STEP_SUMMARY
cat breaking-changes.txt >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Breaking changes require a new major version (/v2). See: docs/api-versioning.md" >> $GITHUB_STEP_SUMMARY
fi
SDK Version Management
// For SDKs that need to support multiple API versions:
// src/sdk/client.ts
type ApiVersion = 'v1' | 'v2';
interface ClientConfig {
baseUrl: string;
apiKey: string;
version?: ApiVersion;
}
export class ApiClient {
private version: ApiVersion;
constructor(private config: ClientConfig) {
this.version = config.version ?? 'v2'; // Default to latest
}
async listUsers(): Promise<User[]> {
if (this.version === 'v1') {
const data = await this.request<any[]>('/users');
// Normalize v1 flat array to standard format
return data.map(u => ({ ...u, firstName: u.name.split(' ')[0], lastName: u.name.split(' ').slice(1).join(' ') }));
}
const { data } = await this.request<{ data: User[]; pagination: any }>('/users');
return data;
}
private async request<T>(path: string): Promise<T> {
const response = await fetch(`${this.config.baseUrl}/${this.version}${path}`, {
headers: { Authorization: `Bearer ${this.config.apiKey}` },
});
if (!response.ok) throw new Error(`API error: ${response.statusText}`);
return response.json();
}
}
For the OpenAPI spec contract testing that catches breaking changes before deployment, see the OpenAPI codegen guide. For API gateway patterns including rate limiting per API version, see the API gateway guide. The Claude Skills 360 bundle includes API design skill sets covering versioning strategies, deprecation workflows, and migration guides. Start with the free tier to try API versioning code generation.