api-versioning-strategy
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI Versioning Strategy
API版本控制策略
Overview
概述
Comprehensive guide to API versioning approaches, deprecation strategies, backward compatibility techniques, and migration planning for REST APIs, GraphQL, and gRPC services.
这是一份针对REST APIs、GraphQL和gRPC服务的API版本控制方法、弃用策略、向后兼容技术以及迁移规划的综合指南。
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
- 从设计初期就为新API加入版本控制
- 为现有API添加破坏性变更
- 弃用旧版API
- 规划API迁移
- 确保向后兼容性
- 同时管理多个API版本
- 为不同版本创建API文档
- 实现API版本路由
Instructions
操作指南
1. Versioning Approaches
1. 版本控制方法
URL Path Versioning
URL路径版本控制
typescript
// 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
typescript
// 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 });
}
});优点: 简单直观、支持缓存
缺点: 污染URL、弃用难度大
Header Versioning (Content Negotiation)
请求头版本控制(内容协商)
typescript
// 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
typescript
// 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));
});优点: URL简洁、符合REST规范
缺点: 可见性低、手动测试难度大
Query Parameter Versioning
查询参数版本控制
typescript
// 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=2Pros: Easy to implement, flexible
Cons: Not RESTful, can be overlooked
typescript
// 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优点: 易于实现、灵活性高
缺点: 不符合REST规范、容易被忽略
2. Backward Compatibility Patterns
2. 向后兼容模式
Additive Changes (Non-Breaking)
增量变更(非破坏性)
typescript
// ✅ 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);
});typescript
// ✅ 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)
破坏性变更(需要新版本)
typescript
// ❌ 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 } }typescript
// ❌ 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
多版本兼容处理
typescript
// 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);
});typescript
// 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
3. 弃用策略
Deprecation Headers
弃用请求头
typescript
// 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);
});typescript
// 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
弃用响应内容
typescript
// 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: [...]
});
});typescript
// 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
渐进式弃用时间线
typescript
// 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();
});typescript
// 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
4. 迁移指南示例
markdown
undefinedmarkdown
undefinedAPI Migration Guide: v1 to v2
API迁移指南:v1到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
Version 2 introduces breaking changes to improve consistency and add new features.
时间线:
- 2024-01-01: v2发布
- 2024-06-01: v1弃用
- 2024-09-01: v1进入只读模式
- 2024-12-31: v1停止服务
Breaking Changes
破坏性变更
1. Response Structure
1. 响应结构
v1:
json
{
"users": [...],
"total": 10,
"page": 1
}v2:
json
{
"data": [...],
"meta": {
"total": 10,
"page": 1,
"perPage": 20
}
}Migration:
javascript
// Before
const users = response.users;
const total = response.total;
// After
const users = response.data;
const total = response.meta.total;v1:
json
{
"users": [...],
"total": 10,
"page": 1
}v2:
json
{
"data": [...],
"meta": {
"total": 10,
"page": 1,
"perPage": 20
}
}迁移方式:
javascript
// Before
const users = response.users;
const total = response.total;
// After
const users = response.data;
const total = response.meta.total;2. Date Format
2. 日期格式
v1: ISO 8601 strings
v2: Unix timestamps
Migration:
javascript
// Before
const created = new Date(user.created);
// After
const created = new Date(user.created * 1000);v1: ISO 8601字符串
v2: Unix时间戳
迁移方式:
javascript
// Before
const created = new Date(user.created);
// After
const created = new Date(user.created * 1000);3. Error Format
3. 错误格式
v1:
json
{ "error": "User not found" }v2:
json
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"details": {}
}
}v1:
json
{ "error": "User not found" }v2:
json
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"details": {}
}
}New Features in v2
v2新增功能
Pagination
分页
javascript
// v2 supports cursor-based pagination
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20javascript
// v2支持基于游标分页
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20Field Selection
字段选择
javascript
// v2 supports field filtering
GET /api/v2/users?fields=id,name,emailjavascript
// v2支持字段过滤
GET /api/v2/users?fields=id,name,emailBatch Operations
批量操作
javascript
// v2 supports batch requests
POST /api/v2/batch
{
"requests": [
{ "method": "GET", "path": "/users/1" },
{ "method": "GET", "path": "/users/2" }
]
}javascript
// v2支持批量请求
POST /api/v2/batch
{
"requests": [
{ "method": "GET", "path": "/users/1" },
{ "method": "GET", "path": "/users/2" }
]
}Code Examples
代码示例
JavaScript/TypeScript
JavaScript/TypeScript
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
}
}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
Python
python
undefinedpython
undefinedv1
v1
response = requests.get(f"{base_url}/api/v1/users")
users = response.json()["users"]
response = requests.get(f"{base_url}/api/v1/users")
users = response.json()["users"]
v2
v2
response = requests.get(f"{base_url}/api/v2/users")
users = response.json()["data"]
undefinedresponse = requests.get(f"{base_url}/api/v2/users")
users = response.json()["data"]
undefined5. GraphQL Versioning
5. GraphQL版本控制
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;
}
}
};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
6. gRPC版本控制
protobuf
// 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);
}protobuf
// 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
7. 版本检测与路由
typescript
// 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());typescript
// 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
8. 多版本测试
typescript
// 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');
});
});
});typescript
// 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
- 从项目初期就引入版本控制(即使是v1)
- 明确文档化破坏性与非破坏性变更
- 提供包含代码示例的清晰迁移指南
- 遵循语义化版本控制原则
- 给出6-12个月的弃用通知期
- 监控旧版API的使用情况
- 向API使用者发送弃用警告
- 同时支持至少2个版本
- 使用适配器/转换器处理版本逻辑
- 测试所有受支持的版本
- 记录正在使用的API版本
- 尽可能提供迁移工具
- 保持版本控制方法的一致性
❌ 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
- 不进行版本控制就修改API行为
- 未提前通知就移除版本
- 支持过多版本(>3个)
- 在同一API中使用不同的版本控制策略
- 不升级版本就破坏API
- 忘记更新文档
- 过快弃用(<6个月)
- 忽略API使用者的反馈
- 每次变更都创建新版本
- 版本号使用不一致
Common Patterns
常见模式
Pattern 1: Version-Agnostic Core
模式1:版本无关核心逻辑
typescript
// 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 { /* ... */ }
}typescript
// 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
模式2:功能标记渐进式发布
typescript
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));
});typescript
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
模式3:API版本指标监控
typescript
// Track usage by version
app.use((req, res, next) => {
const version = detectVersion(req);
metrics.increment('api.requests', { version });
next();
});typescript
// 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
- OpenAPI/Swagger: 支持版本的API文档工具
- Postman: 具备版本管理的API测试工具
- API Blueprint: 支持版本控制的API设计工具
- Stoplight: API设计与文档平台
- Kong: 支持版本路由的API网关