Loading...
Loading...
Implement API versioning strategies including URL versioning, header versioning, backward compatibility, deprecation strategies, and migration guides. Use when dealing with API versions, deprecating endpoints, or managing breaking changes.
npx skill4agent add aj-geddes/useful-ai-prompts api-versioning-strategy// 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: 'john@example.com' }
]
});
});
// 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 });
}
});// 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));
});// 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// ✅ 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: 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 } }// 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);
});// 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);
});// 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: [...]
});
});// 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();
});# 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
}{
"data": [...],
"meta": {
"total": 10,
"page": 1,
"perPage": 20
}
}// Before
const users = response.users;
const total = response.total;
// After
const users = response.data;
const total = response.meta.total;// Before
const created = new Date(user.created);
// After
const created = new Date(user.created * 1000);{ "error": "User not found" }{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"details": {}
}
}// v2 supports cursor-based pagination
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20// v2 supports field filtering
GET /api/v2/users?fields=id,name,email// v2 supports batch requests
POST /api/v2/batch
{
"requests": [
{ "method": "GET", "path": "/users/1" },
{ "method": "GET", "path": "/users/2" }
]
}// 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
}
}# 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;
}
}
};// 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);
}// 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());// 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');
});
});
});// 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 { /* ... */ }
}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));
});// Track usage by version
app.use((req, res, next) => {
const version = detectVersion(req);
metrics.increment('api.requests', { version });
next();
});