api-versioning-strategy
About
This skill helps developers implement API versioning strategies including URL and header approaches. It provides guidance for managing breaking changes, ensuring backward compatibility, and creating deprecation strategies. Use it when versioning APIs, deprecating endpoints, or planning migrations across REST, GraphQL, and gRPC services.
Documentation
API Versioning Strategy
Overview
Comprehensive guide to API versioning approaches, deprecation strategies, backward compatibility techniques, and migration planning for REST APIs, GraphQL, and gRPC services.
When to Use
- Designing new APIs with versioning from the start
- Adding breaking changes to existing APIs
- Deprecating old API versions
- Planning API migrations
- Ensuring backward compatibility
- Managing multiple API versions simultaneously
- Creating API documentation for different versions
- Implementing API version routing
Instructions
1. Versioning Approaches
URL Path Versioning
// express-router.ts
import express from 'express';
const app = express();
// Version 1
app.get('/api/v1/users', (req, res) => {
res.json({
users: [
{ id: 1, name: 'John Doe' }
]
});
});
// Version 2 - Added email field
app.get('/api/v2/users', (req, res) => {
res.json({
users: [
{ id: 1, name: 'John Doe', email: '[email protected]' }
]
});
});
// Shared logic with version-specific transformations
app.get('/api/:version/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (req.params.version === 'v1') {
res.json({ id: user.id, name: user.name });
} else if (req.params.version === 'v2') {
res.json({ id: user.id, name: user.name, email: user.email });
}
});
Pros: Simple, explicit, cache-friendly Cons: URL pollution, harder to deprecate
Header Versioning (Content Negotiation)
// header-versioning.ts
app.get('/api/users', (req, res) => {
const version = req.headers['api-version'] || '1';
switch (version) {
case '1':
return res.json(transformToV1(users));
case '2':
return res.json(transformToV2(users));
default:
return res.status(400).json({ error: 'Unsupported API version' });
}
});
// Or using Accept header
app.get('/api/users', (req, res) => {
const acceptHeader = req.headers['accept'];
if (acceptHeader.includes('application/vnd.myapi.v2+json')) {
return res.json(transformToV2(users));
}
// Default to v1
return res.json(transformToV1(users));
});
Pros: Clean URLs, RESTful Cons: Less visible, harder to test manually
Query Parameter Versioning
// query-param-versioning.ts
app.get('/api/users', (req, res) => {
const version = req.query.version || '1';
if (version === '2') {
return res.json(transformToV2(users));
}
return res.json(transformToV1(users));
});
// Usage: GET /api/users?version=2
Pros: Easy to implement, flexible Cons: Not RESTful, can be overlooked
2. Backward Compatibility Patterns
Additive Changes (Non-Breaking)
// ✅ Safe: Adding optional fields
interface UserV1 {
id: string;
name: string;
}
interface UserV2 extends UserV1 {
email?: string; // Optional field
avatar?: string; // Optional field
}
// ✅ Safe: Adding new endpoints
app.post('/api/v1/users/:id/avatar', uploadAvatar);
// ✅ Safe: Accepting additional parameters
app.get('/api/v1/users', (req, res) => {
const { page, limit, sortBy } = req.query; // New optional params
const users = await userService.list({ page, limit, sortBy });
res.json(users);
});
Breaking Changes (Require New Version)
// ❌ Breaking: Removing fields
interface UserV1 {
id: string;
name: string;
username: string;
}
interface UserV2 {
id: string;
name: string;
// username removed - BREAKING!
}
// ❌ Breaking: Changing field types
interface UserV1 {
id: string;
created: string; // ISO string
}
interface UserV2 {
id: string;
created: number; // Unix timestamp - BREAKING!
}
// ❌ Breaking: Renaming fields
interface UserV1 {
fullName: string;
}
interface UserV2 {
name: string; // Renamed from fullName - BREAKING!
}
// ❌ Breaking: Changing response structure
// V1
{ users: [...], total: 10 }
// V2 - BREAKING!
{ data: [...], meta: { total: 10 } }
Handling Both Versions
// version-adapter.ts
export class UserAdapter {
toV1(user: User): UserV1Response {
return {
id: user.id,
name: user.fullName,
username: user.username,
created: user.createdAt.toISOString()
};
}
toV2(user: User): UserV2Response {
return {
id: user.id,
name: user.fullName,
email: user.email,
profile: {
avatar: user.avatarUrl,
bio: user.bio
},
createdAt: user.createdAt.getTime()
};
}
fromV1(data: UserV1Request): User {
return {
fullName: data.name,
username: data.username,
email: data.email || null
};
}
fromV2(data: UserV2Request): User {
return {
fullName: data.name,
username: data.username || generateUsername(data.email),
email: data.email,
avatarUrl: data.profile?.avatar,
bio: data.profile?.bio
};
}
}
// Usage in controller
app.get('/api/:version/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
const adapter = new UserAdapter();
const response = req.params.version === 'v2'
? adapter.toV2(user)
: adapter.toV1(user);
res.json(response);
});
3. Deprecation Strategy
Deprecation Headers
// deprecation-middleware.ts
export function deprecationWarning(version: string, sunsetDate: Date) {
return (req, res, next) => {
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', sunsetDate.toUTCString());
res.setHeader('Link', '</api/v2/docs>; rel="successor-version"');
res.setHeader('X-API-Warn', `Version ${version} is deprecated. Please migrate to v2 by ${sunsetDate.toDateString()}`);
next();
};
}
// Apply to deprecated routes
app.use('/api/v1/*', deprecationWarning('v1', new Date('2024-12-31')));
app.get('/api/v1/users', (req, res) => {
// Return v1 response with deprecation headers
res.json(users);
});
Deprecation Response
// Include deprecation info in response body
app.get('/api/v1/users', (req, res) => {
res.json({
_meta: {
deprecated: true,
sunsetDate: '2024-12-31',
message: 'This API version is deprecated. Please migrate to v2.',
migrationGuide: 'https://docs.example.com/migration-v1-to-v2'
},
users: [...]
});
});
Gradual Deprecation Timeline
// deprecation-stages.ts
enum DeprecationStage {
SUPPORTED = 'supported',
DEPRECATED = 'deprecated',
SUNSET_ANNOUNCED = 'sunset_announced',
READONLY = 'readonly',
SHUTDOWN = 'shutdown'
}
const versionStatus = {
'v1': {
stage: DeprecationStage.READONLY,
sunsetDate: new Date('2024-06-30'),
message: 'Read-only mode. New writes are disabled.'
},
'v2': {
stage: DeprecationStage.DEPRECATED,
sunsetDate: new Date('2024-12-31'),
message: 'Deprecated. Please migrate to v3.'
},
'v3': {
stage: DeprecationStage.SUPPORTED,
message: 'Current stable version.'
}
};
// Middleware to enforce deprecation
app.use('/api/:version/*', (req, res, next) => {
const status = versionStatus[req.params.version];
if (!status) {
return res.status(404).json({ error: 'API version not found' });
}
if (status.stage === DeprecationStage.SHUTDOWN) {
return res.status(410).json({ error: 'API version no longer available' });
}
if (status.stage === DeprecationStage.READONLY &&
['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
return res.status(403).json({
error: 'API version is read-only',
message: status.message
});
}
// Add deprecation headers
if (status.stage !== DeprecationStage.SUPPORTED) {
res.setHeader('X-API-Deprecated', 'true');
res.setHeader('X-API-Sunset', status.sunsetDate.toISOString());
}
next();
});
4. Migration Guide Example
# API Migration Guide: v1 to v2
## Overview
Version 2 introduces breaking changes to improve consistency and add new features.
**Timeline:**
- 2024-01-01: v2 released
- 2024-06-01: v1 deprecated
- 2024-09-01: v1 read-only
- 2024-12-31: v1 shutdown
## Breaking Changes
### 1. Response Structure
**v1:**
```json
{
"users": [...],
"total": 10,
"page": 1
}
v2:
{
"data": [...],
"meta": {
"total": 10,
"page": 1,
"perPage": 20
}
}
Migration:
// Before
const users = response.users;
const total = response.total;
// After
const users = response.data;
const total = response.meta.total;
2. Date Format
v1: ISO 8601 strings v2: Unix timestamps
Migration:
// Before
const created = new Date(user.created);
// After
const created = new Date(user.created * 1000);
3. Error Format
v1:
{ "error": "User not found" }
v2:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"details": {}
}
}
New Features in v2
Pagination
// v2 supports cursor-based pagination
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20
Field Selection
// v2 supports field filtering
GET /api/v2/users?fields=id,name,email
Batch Operations
// v2 supports batch requests
POST /api/v2/batch
{
"requests": [
{ "method": "GET", "path": "/users/1" },
{ "method": "GET", "path": "/users/2" }
]
}
Code Examples
JavaScript/TypeScript
// v1 Client
class ApiClientV1 {
async getUsers() {
const response = await fetch('/api/v1/users');
const data = await response.json();
return data.users;
}
}
// v2 Client
class ApiClientV2 {
async getUsers() {
const response = await fetch('/api/v2/users');
const data = await response.json();
return data.data; // Changed from .users to .data
}
}
Python
# v1
response = requests.get(f"{base_url}/api/v1/users")
users = response.json()["users"]
# v2
response = requests.get(f"{base_url}/api/v2/users")
users = response.json()["data"]
### 5. **GraphQL Versioning**
```typescript
// GraphQL handles versioning differently - through schema evolution
// schema-v1.graphql
type User {
id: ID!
name: String!
username: String!
}
// schema-v2.graphql (deprecated fields)
type User {
id: ID!
name: String!
username: String! @deprecated(reason: "Use email instead")
email: String!
profile: Profile
}
type Profile {
avatar: String
bio: String
}
// Field deprecation in resolver
const resolvers = {
User: {
username: (user) => {
console.warn('username field is deprecated, use email instead');
return user.email;
}
}
};
6. gRPC Versioning
// v1/user.proto
syntax = "proto3";
package user.v1;
message User {
string id = 1;
string name = 2;
}
// v2/user.proto
syntax = "proto3";
package user.v2;
message User {
string id = 1;
string name = 2;
string email = 3;
Profile profile = 4;
}
message Profile {
string avatar = 1;
string bio = 2;
}
// Both versions can coexist
service UserServiceV1 {
rpc GetUser (GetUserRequest) returns (user.v1.User);
}
service UserServiceV2 {
rpc GetUser (GetUserRequest) returns (user.v2.User);
}
7. Version Detection & Routing
// version-router.ts
import express from 'express';
export class VersionRouter {
private versions = new Map<string, express.Router>();
registerVersion(version: string, router: express.Router) {
this.versions.set(version, router);
}
getMiddleware() {
return (req, res, next) => {
// Detect version from multiple sources
const version = this.detectVersion(req);
const router = this.versions.get(version);
if (!router) {
return res.status(400).json({
error: 'Invalid API version',
supportedVersions: Array.from(this.versions.keys())
});
}
// Set version in request for logging
req.apiVersion = version;
// Use versioned router
router(req, res, next);
};
}
private detectVersion(req): string {
// 1. Check URL path
const pathMatch = req.path.match(/^\/api\/v(\d+)\//);
if (pathMatch) return pathMatch[1];
// 2. Check header
if (req.headers['api-version']) {
return req.headers['api-version'];
}
// 3. Check Accept header
const acceptMatch = req.headers['accept']?.match(/application\/vnd\.myapi\.v(\d+)\+json/);
if (acceptMatch) return acceptMatch[1];
// 4. Check query parameter
if (req.query.version) {
return req.query.version;
}
// 5. Default version
return '1';
}
}
// Usage
const versionRouter = new VersionRouter();
versionRouter.registerVersion('1', v1Router);
versionRouter.registerVersion('2', v2Router);
versionRouter.registerVersion('3', v3Router);
app.use('/api', versionRouter.getMiddleware());
8. Testing Multiple Versions
// api-version.test.ts
describe('API Versioning', () => {
describe('v1', () => {
it('should return user with v1 format', async () => {
const response = await request(app)
.get('/api/v1/users/1')
.expect(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('name');
expect(response.body).not.toHaveProperty('email');
});
});
describe('v2', () => {
it('should return user with v2 format', async () => {
const response = await request(app)
.get('/api/v2/users/1')
.expect(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('name');
expect(response.body).toHaveProperty('email');
expect(response.body).toHaveProperty('profile');
});
it('should include deprecation headers for v1', async () => {
const response = await request(app)
.get('/api/v1/users/1');
expect(response.headers['deprecation']).toBe('true');
expect(response.headers['sunset']).toBeDefined();
});
});
describe('version negotiation', () => {
it('should use version from header', async () => {
const response = await request(app)
.get('/api/users/1')
.set('API-Version', '2')
.expect(200);
expect(response.body).toHaveProperty('email');
});
it('should default to v1 if no version specified', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body).not.toHaveProperty('email');
});
});
});
Best Practices
✅ DO
- Version from day one (even if v1)
- Document breaking vs non-breaking changes
- Provide clear migration guides with code examples
- Use semantic versioning principles
- Give 6-12 months deprecation notice
- Monitor usage of deprecated APIs
- Send deprecation warnings to API consumers
- Support at least 2 versions simultaneously
- Use adapters/transformers for version logic
- Test all supported versions
- Log which API version is being used
- Provide migration tooling when possible
- Be consistent with versioning approach
❌ DON'T
- Change API behavior without versioning
- Remove versions without notice
- Support too many versions (>3)
- Use different versioning strategies in same API
- Break APIs without incrementing version
- Forget to update documentation
- Deprecate too quickly (<6 months)
- Ignore feedback from API consumers
- Make every change a new version
- Use version numbers inconsistently
Common Patterns
Pattern 1: Version-Agnostic Core
// Core logic remains version-agnostic
class UserService {
async getUser(id: string): Promise<User> {
return this.repository.findById(id);
}
}
// Version-specific adapters
class UserV1Adapter {
transform(user: User): UserV1 { /* ... */ }
}
class UserV2Adapter {
transform(user: User): UserV2 { /* ... */ }
}
Pattern 2: Feature Flags for Gradual Rollout
app.get('/api/v2/users', async (req, res) => {
const user = await userService.getUser(req.params.id);
// Gradual rollout of new feature
if (featureFlags.isEnabled('enhanced-profile', req.user.id)) {
return res.json(transformWithEnhancedProfile(user));
}
return res.json(transformV2(user));
});
Pattern 3: API Version Metrics
// Track usage by version
app.use((req, res, next) => {
const version = detectVersion(req);
metrics.increment('api.requests', { version });
next();
});
Tools & Resources
- OpenAPI/Swagger: API documentation with version support
- Postman: API testing with version management
- API Blueprint: API design with versioning
- Stoplight: API design and documentation
- Kong: API gateway with version routing
Quick Install
/plugin add https://github.com/aj-geddes/useful-ai-prompts/tree/main/api-versioning-strategyCopy and paste this command in Claude Code to install this skill
GitHub 仓库
Related Skills
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
Algorithmic Art Generation
MetaThis skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.
webapp-testing
TestingThis Claude Skill provides a Playwright-based toolkit for testing local web applications through Python scripts. It enables frontend verification, UI debugging, screenshot capture, and log viewing while managing server lifecycles. Use it for browser automation tasks but run scripts directly rather than reading their source code to avoid context pollution.
