# API版本控制指南 ## 概述 API版本控制是API设计的重要部分,它允许我们在不破坏现有客户端的情况下演进API。本项目采用URL路径版本控制策略。 ## 版本控制策略 ### URL路径版本控制 使用URL路径中的版本号来区分不同版本的API: ``` /api/v1/endpoint # 版本1 /api/v2/endpoint # 版本2 ``` **优点**: - ✅ 清晰明了,易于理解 - ✅ 便于缓存和路由 - ✅ 支持多版本并存 - ✅ 客户端易于使用 **缺点**: - ❌ URL较长 - ❌ 需要维护多个版本 ### 版本命名规则 - **主版本号**:`v1`, `v2`, `v3`... - **格式**:`/api/v{major}/` - **示例**: - `/api/v1/content` - `/api/v1/admin/users` ## 目录结构 ### 当前结构(向后兼容) ``` src/app/api/ ├── admin/ │ ├── config/ │ ├── content/ │ ├── upload/ │ └── users/ ├── auth/ ├── config/ ├── content/ ├── docs/ └── health/ ``` ### 版本化结构(推荐) ``` src/app/api/ ├── v1/ # 版本1 API │ ├── admin/ │ │ ├── config/ │ │ ├── content/ │ │ ├── upload/ │ │ └── users/ │ ├── auth/ │ ├── config/ │ ├── content/ │ └── health/ ├── admin/ # 向后兼容(重定向到v1) ├── auth/ ├── config/ ├── content/ ├── docs/ # OpenAPI文档(无版本) └── health/ ``` ## 实施步骤 ### 步骤1:创建版本化API #### 创建v1目录 ```bash mkdir -p src/app/api/v1 ``` #### 迁移现有API 将现有API复制到v1目录: ```bash # 复制admin API cp -r src/app/api/admin src/app/api/v1/ # 复制其他API cp -r src/app/api/auth src/app/api/v1/ cp -r src/app/api/config src/app/api/v1/ cp -r src/app/api/content src/app/api/v1/ cp -r src/app/api/health src/app/api/v1/ ``` ### 步骤2:更新API路由 #### 更新v1 API路由 在v1版本的API中,更新路由路径: ```typescript // src/app/api/v1/admin/content/route.ts /** * @openapi * /api/v1/admin/content: * get: * tags: * - Admin * - Content * summary: 获取内容列表 (v1) * description: 管理员获取内容列表,支持分页、筛选和搜索 * operationId: getAdminContentV1 * ... */ export async function GET(request: NextRequest) { // 实现代码 } ``` ### 步骤3:创建向后兼容层 #### 创建重定向中间件 ```typescript // src/middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // 如果访问旧API路径,重定向到v1版本 const legacyApiPaths = [ '/api/admin', '/api/auth', '/api/config', '/api/content', '/api/health', ]; const isLegacyApi = legacyApiPaths.some(path => pathname.startsWith(path) && !pathname.includes('/v1/') ); if (isLegacyApi) { const url = request.nextUrl.clone(); url.pathname = pathname.replace('/api/', '/api/v1/'); // 返回重定向响应(可选:也可以内部重写) // return NextResponse.redirect(url); // 或者内部重写(URL不变,但使用新路径) return NextResponse.rewrite(url); } return NextResponse.next(); } export const config = { matcher: '/api/:path*', }; ``` ### 步骤4:更新客户端代码 #### 更新API客户端 ```typescript // src/lib/api-client.ts const API_VERSION = 'v1'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || ''; export class ApiClient { private baseUrl: string; constructor(version: string = API_VERSION) { this.baseUrl = `${API_BASE_URL}/api/${version}`; } async get(endpoint: string, options?: RequestInit) { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, method: 'GET', }); return response.json(); } async post(endpoint: string, data: any, options?: RequestInit) { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers, }, body: JSON.stringify(data), }); return response.json(); } } // 使用示例 const apiClient = new ApiClient('v1'); const content = await apiClient.get('/admin/content'); ``` ## 版本生命周期 ### 版本状态 | 状态 | 描述 | 持续时间 | |------|------|----------| | **Current** | 当前推荐版本 | 无限期 | | **Supported** | 仍受支持,但不推荐新功能 | 6-12个月 | | **Deprecated** | 即将废弃,计划移除 | 3-6个月 | | **Sunset** | 已移除,不再可用 | - | ### 版本废弃流程 1. **公告**:提前6个月通知废弃计划 2. **警告**:在响应头中添加`Deprecation`和`Sunset`头 3. **迁移期**:提供迁移指南和工具 4. **移除**:在预定日期移除旧版本 #### 添加废弃头 ```typescript // src/app/api/v1/admin/content/route.ts export async function GET(request: NextRequest) { const response = NextResponse.json(data); // 添加废弃警告 response.headers.set('Deprecation', 'true'); response.headers.set('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT'); response.headers.set('Link', '; rel="successor-version"'); return response; } ``` ## 版本间差异处理 ### 向后兼容的变更 以下变更不需要增加主版本号: - ✅ 添加新的可选参数 - ✅ 添加新的响应字段 - ✅ 添加新的端点 - ✅ 修复bug ### 需要新版本的变更 以下变更需要增加主版本号: - ❌ 移除或重命名端点 - ❌ 移除或重命名请求/响应字段 - ❌ 修改必填参数 - ❌ 修改认证方式 - ❌ 修改错误响应格式 ## 多版本并存示例 ### 场景:修改内容API响应格式 #### v1版本(旧) ```typescript // src/app/api/v1/admin/content/route.ts /** * @openapi * /api/v1/admin/content: * get: * responses: * 200: * content: * application/json: * schema: * type: object * properties: * items: * type: array * pagination: * type: object */ export async function GET(request: NextRequest) { const items = await db.select().from(content); return NextResponse.json({ items, pagination: { page: 1, limit: 20, total: items.length }, }); } ``` #### v2版本(新) ```typescript // src/app/api/v2/admin/content/route.ts /** * @openapi * /api/v2/admin/content: * get: * responses: * 200: * content: * application/json: * schema: * type: object * properties: * data: * type: array * meta: * type: object */ export async function GET(request: NextRequest) { const items = await db.select().from(content); return NextResponse.json({ data: items, // 改名:items -> data meta: { // 改名:pagination -> meta page: 1, limit: 20, total: items.length, hasNext: items.length === 20, }, }); } ``` ## 测试策略 ### 版本兼容性测试 ```typescript // src/app/api/__tests__/version-compatibility.test.ts import { describe, it, expect } from '@jest/globals'; describe('API Version Compatibility', () => { it('should return same data structure in v1 and v2', async () => { const v1Response = await fetch('/api/v1/admin/content'); const v2Response = await fetch('/api/v2/admin/content'); const v1Data = await v1Response.json(); const v2Data = await v2Response.json(); // 验证数据一致性 expect(v1Data.items.length).toBe(v2Data.data.length); expect(v1Data.pagination.total).toBe(v2Data.meta.total); }); it('should redirect legacy API to v1', async () => { const response = await fetch('/api/admin/content'); expect(response.url).toContain('/api/v1/admin/content'); }); }); ``` ## 文档更新 ### 更新OpenAPI文档 ```typescript // src/app/api/docs/route.ts const options = { definition: { openapi: '3.0.0', info: { title: '睿新致远 API', version: '1.0.0', description: ` ## API版本 当前支持以下版本: - **v1** (Current): 当前推荐版本 - **v2** (Beta): 测试版本,包含新功能 ### 版本状态 | 版本 | 状态 | 发布日期 | 废弃日期 | |------|------|----------|----------| | v1 | Current | 2024-01-01 | - | | v2 | Beta | 2024-06-01 | - | `, }, servers: [ { url: '/api/v1', description: 'API v1 (Current)', }, { url: '/api/v2', description: 'API v2 (Beta)', }, ], }, }; ``` ## 最佳实践 ### ✅ 推荐做法 1. **提前规划版本策略** - 在API设计初期就考虑版本控制 - 为未来变更预留空间 2. **保持向后兼容** - 尽可能保持旧版本可用 - 提供充足的迁移时间 3. **清晰的文档** - 明确标注版本差异 - 提供迁移指南 4. **版本废弃通知** - 提前通知用户 - 使用HTTP头传递废弃信息 ### ❌ 避免的做法 1. **不要频繁变更主版本** - 主版本变更应该谨慎 - 考虑向后兼容的替代方案 2. **不要突然移除旧版本** - 给用户足够的迁移时间 - 提供迁移工具和文档 3. **不要忽略版本测试** - 确保多版本并存时功能正常 - 测试版本兼容性 ## 监控和分析 ### 版本使用统计 ```typescript // src/lib/api-analytics.ts export async function trackApiVersion(request: NextRequest) { const { pathname } = request.nextUrl; const version = pathname.match(/\/api\/v(\d+)\//)?.[1] || 'legacy'; // 发送到分析服务 await analytics.track('api_request', { version, endpoint: pathname, method: request.method, timestamp: new Date().toISOString(), }); } ``` ### 版本使用报告 定期生成版本使用报告: ```markdown ## API版本使用报告(2024年6月) ### 请求分布 | 版本 | 请求数 | 占比 | 趋势 | |------|--------|------|------| | v1 | 150,000 | 75% | ↓ | | v2 | 50,000 | 25% | ↑ | ### 废弃版本使用 | 版本 | 请求数 | 废弃日期 | 建议 | |------|--------|----------|------| | legacy | 1,000 | 2024-12-31 | 尽快迁移到v1 | ``` ## 参考资源 - [API版本控制最佳实践](https://www.postman.com/api-platform/api-versioning/) - [REST API版本控制](https://restfulapi.net/versioning/) - [语义化版本控制](https://semver.org/) - [HTTP废弃头规范](https://datatracker.ietf.org/doc/html/rfc8594) ## 总结 API版本控制已集成到项目中,提供了: ✅ **清晰的版本管理** ✅ **向后兼容支持** ✅ **平滑的版本迁移** ✅ **版本使用监控** ✅ **完善的文档支持** 通过合理的版本控制策略,可以: - 保护现有客户端 - 安全地演进API - 提供良好的开发者体验 - 维护API的长期健康