api-versioning-strategy

安装量: 129
排名: #6653

安装

npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill api-versioning-strategy

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: '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 }); } });

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

  1. 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

// 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); });

  1. 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', '; 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(); });

  1. 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;

  1. 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);

  1. 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; } } };

  1. 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); }

  1. Version Detection & Routing // version-router.ts import express from 'express';

export class VersionRouter { private versions = new Map();

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());

  1. 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 { 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

返回排行榜