11 KiB
11 KiB
trigger
| trigger |
|---|
| always_on |
API Versioning Strategy
When to Use This Skill
Use this skill when:
- Versioning APIs
- Handling breaking changes
- Implementing API deprecation
- Maintaining backward compatibility
- Implementing version negotiation
- Managing multiple API versions
- Planning API evolution
- Communicating API changes to consumers
Core Concepts
Versioning Strategies
- URL Path Versioning:
/api/v1/users,/api/v2/users - Header Versioning:
Accept: application/vnd.goodgo.v1+json - Query Parameter:
/api/users?version=1 - Semantic Versioning: Major.Minor.Patch (e.g., 1.2.3)
Compatibility Types
- Backward Compatible: New version works with old clients
- Forward Compatible: Old version works with new clients
- Breaking Changes: Incompatible changes requiring new version
URL Path Versioning
Implementation
// src/routes/index.ts
// EN: Route versioning
// VI: Route versioning
import { Router } from 'express';
import v1Router from './v1';
import v2Router from './v2';
const router = Router();
// EN: Version 1 routes
// VI: Routes version 1
router.use('/v1', v1Router);
// EN: Version 2 routes
// VI: Routes version 2
router.use('/v2', v2Router);
export default router;
Version Router
// src/routes/v1/index.ts
// EN: Version 1 routes
// VI: Routes version 1
import { Router } from 'express';
import userRoutes from './users';
const router = Router();
router.use('/users', userRoutes);
export default router;
// src/routes/v2/index.ts
// EN: Version 2 routes with breaking changes
// VI: Routes version 2 với breaking changes
import { Router } from 'express';
import userRoutes from './users'; // EN: Different implementation / VI: Implementation khác
const router = Router();
router.use('/users', userRoutes); // EN: Different response format / VI: Format response khác
export default router;
Header-Based Versioning
Version Negotiation Middleware
// src/middlewares/version-negotiation.middleware.ts
// EN: Version negotiation middleware
// VI: Middleware version negotiation
import { Request, Response, NextFunction } from 'express';
import { logger } from '@goodgo/logger';
export function versionNegotiation(
req: Request,
res: Response,
next: NextFunction
): void {
// EN: Extract version from Accept header
// VI: Trích xuất version từ Accept header
const acceptHeader = req.headers.accept || '';
const versionMatch = acceptHeader.match(/application\/vnd\.goodgo\.v(\d+)\+json/);
if (versionMatch) {
const requestedVersion = parseInt(versionMatch[1], 10);
req.apiVersion = requestedVersion;
// EN: Check if version is supported
// VI: Kiểm tra xem version có được hỗ trợ không
const supportedVersions = [1, 2];
if (!supportedVersions.includes(requestedVersion)) {
return res.status(400).json({
success: false,
error: {
code: 'UNSUPPORTED_VERSION',
message: `API version ${requestedVersion} is not supported. Supported versions: ${supportedVersions.join(', ')}`,
},
});
}
} else {
// EN: Default to latest version
// VI: Mặc định version mới nhất
req.apiVersion = 2;
}
next();
}
Version-Aware Controller
// src/modules/user/user.controller.ts
// EN: Version-aware controller
// VI: Controller nhận biết version
export class UserController {
async getUser(req: Request, res: Response): Promise<void> {
const version = req.apiVersion || 2;
if (version === 1) {
// EN: Version 1 response format
// VI: Format response version 1
const user = await this.userService.findById(req.params.id);
res.json({
id: user.id,
email: user.email,
name: user.name,
});
} else {
// EN: Version 2 response format
// VI: Format response version 2
const user = await this.userService.findById(req.params.id);
res.json({
success: true,
data: {
user: {
id: user.id,
email: user.email,
name: user.name,
profile: user.profile, // EN: New field in v2 / VI: Field mới trong v2
},
},
metadata: {
version: '2.0.0',
timestamp: new Date().toISOString(),
},
});
}
}
}
Semantic Versioning
Version Structure
MAJOR.MINOR.PATCH
MAJOR: Breaking changes
MINOR: Backward-compatible additions
PATCH: Backward-compatible bug fixes
Version Response
// src/core/api/version.middleware.ts
// EN: Add version to response
// VI: Thêm version vào response
export function versionMiddleware(req: Request, res: Response, next: NextFunction): void {
const originalJson = res.json.bind(res);
res.json = (data: any) => {
const response = {
...data,
metadata: {
...data.metadata,
apiVersion: req.apiVersion || '2.0.0',
serviceVersion: process.env.SERVICE_VERSION || '1.0.0',
},
};
return originalJson(response);
};
next();
}
API Deprecation
Deprecation Headers
// src/middlewares/deprecation.middleware.ts
// EN: Deprecation warning middleware
// VI: Middleware cảnh báo deprecation
export function deprecationMiddleware(version: string, sunsetDate: string) {
return (req: Request, res: Response, next: NextFunction): void => {
if (req.apiVersion && parseInt(req.apiVersion.toString()) < parseInt(version)) {
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', sunsetDate);
res.setHeader('Link', `<${req.url.replace(/\/v\d+/, `/v${version}`)}>; rel="successor-version"`);
res.setHeader('Warning', `299 - "API version ${req.apiVersion} is deprecated. Please migrate to version ${version} by ${sunsetDate}"`);
}
next();
};
}
// Usage
router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);
Deprecation Documentation
// src/docs/deprecation.md
// EN: Deprecation notices
// VI: Thông báo deprecation
## API Version 1 Deprecation
**Deprecated**: 2024-01-01
**Sunset Date**: 2024-12-31
**Migration Guide**: [Migration Guide](./migration-v1-to-v2.md)
### Changes in Version 2
- New response format
- Additional user profile fields
- Updated authentication flow
Backward Compatibility
Compatibility Layer
// src/core/api/compatibility.adapter.ts
// EN: Backward compatibility adapter
// VI: Adapter tương thích ngược
export class CompatibilityAdapter {
/**
* EN: Adapt v1 response to v2 format
* VI: Adapt response v1 sang format v2
*/
adaptV1ToV2(v1Data: any): any {
return {
success: true,
data: {
user: {
...v1Data,
profile: null, // EN: Add default for new field / VI: Thêm mặc định cho field mới
},
},
metadata: {
version: '2.0.0',
adapted: true,
},
};
}
/**
* EN: Adapt v2 request to v1 format
* VI: Adapt request v2 sang format v1
*/
adaptV2RequestToV1(v2Request: any): any {
return {
email: v2Request.email,
name: v2Request.name,
// EN: Ignore new fields / VI: Bỏ qua các field mới
};
}
}
Breaking Changes Migration
Migration Strategy
// src/core/api/migration.strategy.ts
// EN: Migration strategy for breaking changes
// VI: Chiến lược migration cho breaking changes
export class MigrationStrategy {
/**
* EN: Phase 1: Support both versions
* VI: Giai đoạn 1: Hỗ trợ cả hai versions
*/
phase1SupportBoth(): void {
// EN: Keep v1, add v2
// VI: Giữ v1, thêm v2
router.use('/v1', v1Router);
router.use('/v2', v2Router);
}
/**
* EN: Phase 2: Deprecate v1
* VI: Giai đoạn 2: Deprecate v1
*/
phase2DeprecateV1(): void {
router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);
router.use('/v2', v2Router);
}
/**
* EN: Phase 3: Remove v1
* VI: Giai đoạn 3: Xóa v1
*/
phase3RemoveV1(): void {
// EN: After sunset date, remove v1 routes
// VI: Sau sunset date, xóa v1 routes
router.use('/v2', v2Router);
}
}
Best Practices
- Versioning Strategy: Choose URL path or header, be consistent
- Semantic Versioning: Use MAJOR.MINOR.PATCH
- Deprecation: Always deprecate before removing
- Migration Guide: Provide clear migration documentation
- Backward Compatibility: Maintain compatibility when possible
- Communication: Clearly communicate version changes
Common Mistakes
-
No Deprecation Period: Breaking clients suddenly
// ❌ BAD: Remove v1 immediately router.use('/v2', v2Router); // ✅ GOOD: Deprecate with sunset date router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router); router.use('/v2', v2Router); -
Breaking Changes Without Major Version: Client confusion
# ❌ BAD: Breaking change in minor version v1.1.0 → Changed response format # ✅ GOOD: Breaking change = new major version v1.x.x → v2.0.0 (new response format) -
Inconsistent Versioning Strategy: Mixed approaches
// ❌ BAD: Mix URL and header versioning /api/v1/users + Accept: application/vnd.v2+json // ✅ GOOD: Choose one approach /api/v1/users OR Accept: application/vnd.goodgo.v1+json -
No Migration Guide: Clients don't know how to upgrade
# ✅ Always provide: - Changelog - Migration guide - Sunset date - Deprecation warnings
Quick Reference
| Strategy | Pros | Cons | Use When |
|---|---|---|---|
| URL Path | Clear, cacheable | URL changes | Public APIs |
| Header | Clean URLs | Less visible | Internal APIs |
| Query Param | Simple | Not RESTful | Quick prototypes |
Semantic Versioning:
MAJOR.MINOR.PATCH
│ │ └── Bug fixes (backward compatible)
│ └──────── New features (backward compatible)
└────────────── Breaking changes
Version Lifecycle:
v1 Active → v2 Released → v1 Deprecated → v1 Sunset → v1 Removed
│ │ │ │ │
│ │ Add headers Remove from Delete
│ Support + warnings docs routes
Solo both
Deprecation Headers:
Deprecation: true
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Warning: 299 - "API v1 is deprecated. Migrate to v2 by 2024-12-31"
Link: </api/v2/users>; rel="successor-version"
Version Detection:
// URL path
const version = req.path.match(/\/v(\d+)\//)?.[1] || '2';
// Header
const version = req.headers.accept?.match(/vnd\.goodgo\.v(\d+)/)?.[1];