commit 8462bf45503e1c9779e20815f84eba142275cf94 Author: wehub-skill-sync Date: Mon Jul 13 21:36:01 2026 +0800 chore: import zh skill api-designer diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..52ab1c7 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,9 @@ +# WeHub 来源说明 + +- Skill 名称:`api-designer` +- 中文类目:API 设计原则与最佳实践 +- 上游仓库:`jeffallan__claude-skills` +- 上游路径:`skills/api-designer/SKILL.md` +- 上游链接:https://github.com/jeffallan/claude-skills/blob/HEAD/skills/api-designer/SKILL.md +- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理 +- 原作者、版权和许可证信息以上游仓库为准 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..1c4ab05 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,219 @@ +--- +name: api-designer +description: 用于设计 REST 或 GraphQL API、创建 OpenAPI 规范或规划 API 架构。适用于资源建模、版本策略、分页模式、错误处理标准。 +license: MIT +metadata: + author: https://github.com/Jeffallan + version: "1.1.0" + domain: api-architecture + triggers: API design, REST API, OpenAPI, API specification, API architecture, resource modeling, API versioning, GraphQL schema, API documentation + role: architect + scope: design + output-format: specification + related-skills: graphql-architect, fastapi-expert, nestjs-expert, spring-boot-engineer, security-reviewer +--- + +# API Designer + +资深 API 架构师,专注于 REST 与 GraphQL API,能够编写全面的 OpenAPI 3.1 规范。 + +## 核心工作流 + +1. **分析领域** —— 理解业务需求、数据模型及客户端需求 +2. **建模资源** —— 识别资源、关系与操作;在编写任何规范之前先绘制实体关系图 +3. **设计端点** —— 定义 URI 模式、HTTP 方法、请求/响应模式 +4. **明确契约** —— 创建 OpenAPI 3.1 规范;在继续前进行验证:`npx @redocly/cli lint openapi.yaml` +5. **模拟与验证** —— 启动模拟服务器来测试契约:`npx @stoplight/prism-cli mock openapi.yaml` +6. **规划演进** —— 设计版本管理、废弃策略及向后兼容性方案 + +## 参考指南 + +根据上下文加载详细指导: + +| 主题 | 参考文件 | 加载时机 | +|-------|-----------|-----------| +| REST 模式 | `references/rest-patterns.md` | 资源设计、HTTP 方法、HATEOAS | +| 版本管理 | `references/versioning.md` | API 版本、废弃策略、破坏性变更 | +| 分页 | `references/pagination.md` | 游标、偏移量、keyset 分页 | +| 错误处理 | `references/error-handling.md` | 错误响应、RFC 7807、状态码 | +| OpenAPI | `references/openapi.md` | OpenAPI 3.1、文档、代码生成 | + +## 约束 + +### 必须执行 +- 遵循 REST 原则(面向资源、使用正确的 HTTP 方法) +- 使用一致的命名约定(snake_case 或 camelCase —— 选择一种并全局统一) +- 包含完整的 OpenAPI 3.1 规范 +- 设计正确的错误响应并附带可操作的消息(RFC 7807) +- 对所有集合类端点实现分页 +- 以明确的废弃策略对 API 进行版本管理 +- 记录认证与授权机制 +- 提供请求/响应示例 + +### 禁止事项 +- 在资源 URI 中使用动词(使用 `/users/{id}`,而非 `/getUser/{id}`) +- 返回不一致的响应结构 +- 跳过错误码的文档说明 +- 忽略 HTTP 状态码语义 +- 在设计 API 时不附带版本策略 +- 在 API 表面暴露实现细节 +- 在未提供迁移路径的情况下引入破坏性变更 +- 遗漏速率限制方面的考量 + +## 模板 + +### OpenAPI 3.1 资源端点(可直接复制使用的起始模板) + +```yaml +openapi: "3.1.0" +info: + title: Example API + version: "1.1.0" +paths: + /users: + get: + summary: 获取用户列表 + operationId: listUsers + tags: [Users] + parameters: + - name: cursor + in: query + schema: { type: string } + description: 用于分页的不透明游标 + - name: limit + in: query + schema: { type: integer, default: 20, maximum: 100 } + responses: + "200": + description: 分页的用户列表 + content: + application/json: + schema: + type: object + required: [data, pagination] + properties: + data: + type: array + items: { $ref: "#/components/schemas/User" } + pagination: + $ref: "#/components/schemas/CursorPage" + "400": { $ref: "#/components/responses/BadRequest" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/TooManyRequests" } + /users/{id}: + get: + summary: 获取单个用户 + operationId: getUser + tags: [Users] + parameters: + - name: id + in: path + required: true + schema: { type: string, format: uuid } + responses: + "200": + description: 用户已找到 + content: + application/json: + schema: { $ref: "#/components/schemas/User" } + "404": { $ref: "#/components/responses/NotFound" } + +components: + schemas: + User: + type: object + required: [id, email, created_at] + properties: + id: { type: string, format: uuid, readOnly: true } + email: { type: string, format: email } + name: { type: string } + created_at: { type: string, format: date-time, readOnly: true } + + CursorPage: + type: object + required: [next_cursor, has_more] + properties: + next_cursor: { type: string, nullable: true } + has_more: { type: boolean } + + Problem: # RFC 7807 问题详情 + type: object + required: [type, title, status] + properties: + type: { type: string, format: uri, example: "https://api.example.com/errors/validation-error" } + title: { type: string, example: "Validation Error" } + status: { type: integer, example: 400 } + detail: { type: string, example: "The 'email' field must be a valid email address." } + instance: { type: string, format: uri, example: "/users/req-abc123" } + + responses: + BadRequest: + description: 无效的请求参数 + content: + application/problem+json: + schema: { $ref: "#/components/schemas/Problem" } + Unauthorized: + description: 缺少或无效的认证信息 + content: + application/problem+json: + schema: { $ref: "#/components/schemas/Problem" } + NotFound: + description: 未找到资源 + content: + application/problem+json: + schema: { $ref: "#/components/schemas/Problem" } + TooManyRequests: + description: 超出速率限制 + headers: + Retry-After: { schema: { type: integer } } + content: + application/problem+json: + schema: { $ref: "#/components/schemas/Problem" } + + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + +security: + - BearerAuth: [] +``` + +### RFC 7807 错误响应(可直接复制使用) + +```json +{ + "type": "https://api.example.com/errors/validation-error", + "title": "Validation Error", + "status": 422, + "detail": "The 'email' field must be a valid email address.", + "instance": "/users/req-abc123", + "errors": [ + { "field": "email", "message": "Must be a valid email address." } + ] +} +``` + +- 错误响应始终使用 `Content-Type: application/problem+json`。 +- `type` 必须是稳定、有文档的 URI —— 切勿使用通用字符串。 +- `detail` 必须是人可读且可操作的。 +- 通过 `errors[]` 扩展字段级别的校验失败信息。 + +## 输出检查清单 + +交付 API 设计时,请提供: +1. 资源模型及关系(图表或表格) +2. 包含 URI 与 HTTP 方法的端点规范 +3. OpenAPI 3.1 规范(YAML) +4. 认证与授权流程 +5. 错误响应目录(所有 4xx/5xx 状态码及对应的 `type` URI) +6. 分页与过滤模式 +7. 版本管理与废弃策略 +8. 验证结果:`npx @redocly/cli lint openapi.yaml` 通过且无错误 + +## 知识参考 + +REST 架构、OpenAPI 3.1、GraphQL、HTTP 语义、JSON:API、HATEOAS、OAuth 2.0、JWT、RFC 7807 问题详情、API 版本管理模式、分页策略、速率限制、Webhook 设计、SDK 生成 + +[文档](https://jeffallan.github.io/claude-skills/skills/api-architecture/api-designer/) diff --git a/references/error-handling.md b/references/error-handling.md new file mode 100644 index 0000000..d3006c6 --- /dev/null +++ b/references/error-handling.md @@ -0,0 +1,541 @@ +# API 错误处理 + +## 错误响应设计 + +一致且信息丰富的错误响应对于 API 的可用性至关重要。 + +## 标准错误格式 + +### 基础错误响应 + +```json +{ + "error": { + "code": "RESOURCE_NOT_FOUND", + "message": "未找到 ID 为 123 的用户", + "details": null + } +} +``` + +### RFC 7807 问题详情 + +标准化错误格式 (application/problem+json): + +```http +HTTP/1.1 404 Not Found +Content-Type: application/problem+json + +{ + "type": "https://api.example.com/errors/resource-not-found", + "title": "资源未找到", + "status": 404, + "detail": "ID 为 123 的用户不存在", + "instance": "/users/123" +} +``` + +**字段说明:** +- `type` — 标识错误类型的 URI 引用 +- `title` — 简短、人类可读的摘要 +- `status` — HTTP 状态码 +- `detail` — 针对本次错误的人类可读解释 +- `instance` — 指向本次错误具体实例的 URI 引用 + +### 扩展错误响应 + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "请求验证失败", + "details": [ + { + "field": "email", + "code": "INVALID_FORMAT", + "message": "邮箱必须是有效的电子邮件地址" + }, + { + "field": "age", + "code": "OUT_OF_RANGE", + "message": "年龄必须在 18 到 120 之间" + } + ], + "request_id": "req_123456", + "timestamp": "2024-01-15T10:30:00Z", + "documentation_url": "https://api.example.com/docs/errors#validation-error" + } +} +``` + +## 错误类别 + +### 1. 验证错误(400 Bad Request) + +客户端发送了无效数据。 + +```http +POST /users +Content-Type: application/json + +{ + "name": "", + "email": "invalid-email", + "age": 15 +} + +Response: 400 Bad Request +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "请求验证失败", + "details": [ + { + "field": "name", + "code": "REQUIRED", + "message": "姓名为必填项" + }, + { + "field": "email", + "code": "INVALID_FORMAT", + "message": "邮箱必须是有效的电子邮件地址" + }, + { + "field": "age", + "code": "OUT_OF_RANGE", + "message": "年龄必须至少为 18", + "constraints": { + "min": 18, + "max": 120 + } + } + ] + } +} +``` + +### 2. 身份认证错误(401 Unauthorized) + +缺少或提供了无效的身份认证凭据。 + +```http +GET /users/123 +Authorization: Bearer invalid_token + +Response: 401 Unauthorized +WWW-Authenticate: Bearer realm="api", error="invalid_token" + +{ + "error": { + "code": "INVALID_TOKEN", + "message": "访问令牌无效或已过期", + "details": { + "reason": "token_expired", + "expired_at": "2024-01-15T10:00:00Z" + } + } +} +``` + +**常见认证错误码:** +- `MISSING_TOKEN` — 未提供认证令牌 +- `INVALID_TOKEN` — 令牌格式错误或无效 +- `EXPIRED_TOKEN` — 令牌已过期 +- `REVOKED_TOKEN` — 令牌已被撤销 + +### 3. 授权错误(403 Forbidden) + +已通过身份认证但无权执行操作。 + +```http +DELETE /users/123 +Authorization: Bearer valid_token + +Response: 403 Forbidden +{ + "error": { + "code": "INSUFFICIENT_PERMISSIONS", + "message": "您没有权限删除此用户", + "details": { + "required_permission": "users:delete", + "your_permissions": ["users:read", "users:update"] + } + } +} +``` + +### 4. 未找到错误(404 Not Found) + +资源不存在。 + +```http +GET /users/99999 + +Response: 404 Not Found +{ + "error": { + "code": "RESOURCE_NOT_FOUND", + "message": "未找到 ID 为 99999 的用户", + "details": { + "resource_type": "User", + "resource_id": "99999" + } + } +} +``` + +### 5. 冲突错误(409 Conflict) + +请求与当前状态冲突。 + +```http +POST /users +Content-Type: application/json + +{ + "email": "existing@example.com", + "name": "John Doe" +} + +Response: 409 Conflict +{ + "error": { + "code": "RESOURCE_ALREADY_EXISTS", + "message": "邮箱 'existing@example.com' 对应的用户已存在", + "details": { + "field": "email", + "value": "existing@example.com", + "existing_resource": "/users/123" + } + } +} +``` + +### 6. 速率限制(429 Too Many Requests) + +客户端超出了速率限制。 + +```http +GET /users + +Response: 429 Too Many Requests +Retry-After: 60 +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1705320000 + +{ + "error": { + "code": "RATE_LIMIT_EXCEEDED", + "message": "您已超出速率限制", + "details": { + "limit": 100, + "window": "1 hour", + "retry_after": 60, + "reset_at": "2024-01-15T11:00:00Z" + } + } +} +``` + +### 7. 服务器错误(500 Internal Server Error) + +意外的服务器错误。 + +```http +GET /users/123 + +Response: 500 Internal Server Error +{ + "error": { + "code": "INTERNAL_SERVER_ERROR", + "message": "发生意外错误,请稍后重试。", + "request_id": "req_123456", + "timestamp": "2024-01-15T10:30:00Z" + } +} +``` + +**切勿暴露:** +- 堆栈跟踪 +- 数据库错误 +- 内部路径 +- 敏感配置 + +### 8. 服务不可用(503 Service Unavailable) + +服务暂时不可用。 + +```http +GET /users + +Response: 503 Service Unavailable +Retry-After: 300 + +{ + "error": { + "code": "SERVICE_UNAVAILABLE", + "message": "服务因维护暂时不可用", + "details": { + "retry_after": 300, + "maintenance_end": "2024-01-15T12:00:00Z" + } + } +} +``` + +## 错误码目录 + +为您的 API 定义标准错误码: + +```json +{ + "VALIDATION_ERROR": { + "status": 400, + "description": "请求验证失败", + "subcodes": { + "REQUIRED": "缺少必填字段", + "INVALID_FORMAT": "字段格式无效", + "OUT_OF_RANGE": "值超出允许范围", + "INVALID_ENUM": "值不在允许集合中" + } + }, + "AUTHENTICATION_ERROR": { + "status": 401, + "description": "身份认证失败", + "subcodes": { + "MISSING_TOKEN": "未提供认证令牌", + "INVALID_TOKEN": "令牌无效", + "EXPIRED_TOKEN": "令牌已过期" + } + }, + "AUTHORIZATION_ERROR": { + "status": 403, + "description": "权限不足", + "subcodes": { + "INSUFFICIENT_PERMISSIONS": "缺少所需权限", + "RESOURCE_FORBIDDEN": "禁止访问资源" + } + }, + "RESOURCE_NOT_FOUND": { + "status": 404, + "description": "资源未找到" + }, + "CONFLICT_ERROR": { + "status": 409, + "description": "请求与当前状态冲突", + "subcodes": { + "RESOURCE_ALREADY_EXISTS": "资源已存在", + "CONCURRENT_MODIFICATION": "资源已被其他请求修改" + } + }, + "RATE_LIMIT_EXCEEDED": { + "status": 429, + "description": "超出速率限制" + }, + "INTERNAL_SERVER_ERROR": { + "status": 500, + "description": "内部服务器错误" + } +} +``` + +## 验证错误详情 + +### 字段级验证 + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "请求验证失败", + "details": [ + { + "field": "credit_card.number", + "code": "INVALID_FORMAT", + "message": "信用卡号必须为 16 位数字", + "value_provided": "1234", + "constraints": { + "pattern": "^[0-9]{16}$" + } + }, + { + "field": "items[0].quantity", + "code": "OUT_OF_RANGE", + "message": "数量必须至少为 1", + "value_provided": 0, + "constraints": { + "min": 1, + "max": 1000 + } + } + ] + } +} +``` + +### 跨字段验证 + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "请求验证失败", + "details": [ + { + "fields": ["start_date", "end_date"], + "code": "INVALID_RANGE", + "message": "结束日期必须在开始日期之后", + "values_provided": { + "start_date": "2024-01-20", + "end_date": "2024-01-15" + } + } + ] + } +} +``` + +## 请求 ID 追踪 + +始终包含请求 ID 以便调试: + +```http +Response Headers: +X-Request-ID: req_abc123 + +Response Body: +{ + "error": { + "code": "INTERNAL_SERVER_ERROR", + "message": "发生意外错误", + "request_id": "req_abc123" + } +} +``` + +客户端可在工单中引用请求 ID。 + +## 错误文档 + +记录每个端点所有可能的错误: + +```yaml +/users/{id}: + get: + responses: + '200': + description: 成功 + '401': + description: 身份认证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + missing_token: + value: + error: + code: MISSING_TOKEN + message: 未提供认证令牌 + invalid_token: + value: + error: + code: INVALID_TOKEN + message: 令牌无效或已过期 + '404': + description: 用户未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + not_found: + value: + error: + code: RESOURCE_NOT_FOUND + message: 未找到 ID 为 123 的用户 +``` + +## 重试指导 + +帮助客户端判断是否应该重试: + +```json +{ + "error": { + "code": "SERVICE_UNAVAILABLE", + "message": "服务暂时不可用", + "retry": { + "retryable": true, + "retry_after": 60, + "max_retries": 3, + "backoff": "exponential" + } + } +} +``` + +### 可重试的错误 + +- 408 Request Timeout +- 429 Too Many Requests(带 Retry-After) +- 500 Internal Server Error(部分情况) +- 502 Bad Gateway +- 503 Service Unavailable +- 504 Gateway Timeout + +### 不可重试的错误 + +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found +- 409 Conflict +- 422 Unprocessable Entity + +## 多语言支持 + +支持多语言错误消息: + +```http +GET /users/invalid +Accept-Language: es + +Response: 404 Not Found +Content-Language: es +{ + "error": { + "code": "RESOURCE_NOT_FOUND", + "message": "Usuario con ID 'invalid' no encontrado" + } +} +``` + +始终包含 `code` 字段,以便客户端实现自己的翻译。 + +## 最佳实践 + +1. **使用标准 HTTP 状态码** — 不要在出错时返回 200 +2. **包含机器可读的错误码** — 供客户端逻辑使用的错误码 +3. **提供人类可读的消息** — 清晰的解释说明 +4. **具体但安全** — 不要暴露敏感信息 +5. **包含请求 ID** — 用于追踪和调试 +6. **记录所有错误** — 每个端点每种可能的错误 +7. **保持一致性** — 所有端点使用相同格式 +8. **帮助客户端重试** — 指明错误是否可重试 +9. **尽早验证** — 立即返回验证错误 +10. **在服务端记录错误** — 追踪错误以进行监控 + +## 反模式 + +避免以下错误做法: + +- **通用的错误消息** — 没有详细信息的"发生错误" +- **暴露堆栈跟踪** — 安全风险 +- **不一致的错误格式** — 不同端点使用不同结构 +- **缺少错误码** — 只有人类可读的消息 +- **错误的状态码** — 在响应体中返回错误时状态码为 200 +- **没有请求 ID** — 导致无法调试 +- **未记录的错误** — 客户端不知道会遇到什么 +- **信息过多** — 暴露内部实现细节 diff --git a/references/openapi.md b/references/openapi.md new file mode 100644 index 0000000..0dee823 --- /dev/null +++ b/references/openapi.md @@ -0,0 +1,824 @@ +# OpenAPI 3.1 规范 + +## 什么是 OpenAPI? + +OpenAPI(原名 Swagger)是一种用于描述 REST API 的标准。它支持: +- 交互式文档 +- 代码生成(SDK、客户端、服务端) +- API 测试工具 +- 契约验证 +- 模拟服务器 + +## 基本结构 + +### 最简 OpenAPI 3.1 规范 + +```yaml +openapi: 3.1.0 +info: + title: My API + version: 1.0.0 + description: A sample API + contact: + name: API Support + email: support@example.com + url: https://example.com/support + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + +servers: + - url: https://api.example.com/v1 + description: Production server + - url: https://staging-api.example.com/v1 + description: Staging server + - url: http://localhost:3000/v1 + description: Local development + +paths: + /users: + get: + summary: List users + description: Retrieve a paginated list of users + operationId: listUsers + tags: + - Users + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + +components: + schemas: + User: + type: object + required: + - id + - email + properties: + id: + type: integer + format: int64 + example: 123 + email: + type: string + format: email + example: john@example.com + name: + type: string + example: John Doe +``` + +## Info 对象 + +关于 API 的元数据: + +```yaml +info: + title: Users API + version: 1.0.0 + description: | + # Users API + + This API manages user accounts and profiles. + + ## Features + - User CRUD operations + - Authentication with JWT + - Role-based authorization + + termsOfService: https://example.com/terms + + contact: + name: API Support Team + email: api-support@example.com + url: https://example.com/support + + license: + name: MIT + url: https://opensource.org/licenses/MIT + + x-api-id: users-api-v1 + x-audience: external +``` + +## Servers(服务器) + +定义 API 基础 URL: + +```yaml +servers: + - url: https://api.example.com/v1 + description: Production + variables: + version: + default: v1 + enum: + - v1 + - v2 + + - url: https://{environment}.example.com/v1 + description: Dynamic environment + variables: + environment: + default: api + enum: + - api + - staging + - dev +``` + +## Paths and Operations(路径与操作) + +### 完整端点示例 + +```yaml +paths: + /users: + get: + summary: List users + description: Retrieve a paginated list of users with optional filtering + operationId: listUsers + tags: + - Users + + parameters: + - name: offset + in: query + description: Number of items to skip + required: false + schema: + type: integer + minimum: 0 + default: 0 + + - name: limit + in: query + description: Maximum number of items to return + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + + - name: status + in: query + description: Filter by user status + required: false + schema: + type: string + enum: + - active + - inactive + - suspended + + security: + - bearerAuth: [] + + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/UserListResponse' + examples: + success: + $ref: '#/components/examples/UserListSuccess' + + '401': + $ref: '#/components/responses/Unauthorized' + + '429': + $ref: '#/components/responses/RateLimitExceeded' + + post: + summary: Create user + description: Create a new user account + operationId: createUser + tags: + - Users + + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserRequest' + examples: + basic: + $ref: '#/components/examples/CreateUserBasic' + + responses: + '201': + description: User created successfully + headers: + Location: + description: URL of the created user + schema: + type: string + format: uri + content: + application/json: + schema: + $ref: '#/components/schemas/User' + + '400': + $ref: '#/components/responses/ValidationError' + + '409': + description: User already exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /users/{userId}: + parameters: + - name: userId + in: path + description: User ID + required: true + schema: + type: integer + format: int64 + + get: + summary: Get user + description: Retrieve a specific user by ID + operationId: getUser + tags: + - Users + + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + + '404': + $ref: '#/components/responses/NotFound' + + put: + summary: Update user + description: Replace user data + operationId: updateUser + tags: + - Users + + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRequest' + + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + + '404': + $ref: '#/components/responses/NotFound' + + delete: + summary: Delete user + description: Delete a user account + operationId: deleteUser + tags: + - Users + + responses: + '204': + description: User deleted successfully + + '404': + $ref: '#/components/responses/NotFound' +``` + +## Components(组件) + +API 规范的可复用组件。 + +### Schemas(模式) + +```yaml +components: + schemas: + User: + type: object + required: + - id + - email + - name + properties: + id: + type: integer + format: int64 + readOnly: true + example: 123 + email: + type: string + format: email + example: john@example.com + name: + type: string + minLength: 1 + maxLength: 100 + example: John Doe + status: + type: string + enum: + - active + - inactive + - suspended + default: active + created_at: + type: string + format: date-time + readOnly: true + example: "2024-01-15T10:30:00Z" + metadata: + type: object + additionalProperties: + type: string + + CreateUserRequest: + type: object + required: + - email + - name + properties: + email: + type: string + format: email + name: + type: string + minLength: 1 + maxLength: 100 + metadata: + type: object + additionalProperties: + type: string + + UserListResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + pagination: + $ref: '#/components/schemas/Pagination' + + Pagination: + type: object + properties: + offset: + type: integer + minimum: 0 + limit: + type: integer + minimum: 1 + total: + type: integer + minimum: 0 + has_more: + type: boolean + + Error: + type: object + required: + - error + properties: + error: + type: object + required: + - code + - message + properties: + code: + type: string + example: RESOURCE_NOT_FOUND + message: + type: string + example: User with ID 123 not found + details: + type: object + request_id: + type: string + example: req_abc123 +``` + +### Security Schemes(安全方案) + +```yaml +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT access token + + apiKey: + type: apiKey + in: header + name: X-API-Key + description: API key for authentication + + oauth2: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://auth.example.com/oauth/authorize + tokenUrl: https://auth.example.com/oauth/token + scopes: + users:read: Read user data + users:write: Create and update users + users:delete: Delete users +``` + +全局或按操作应用安全策略: + +```yaml +# Global security +security: + - bearerAuth: [] + +# Or per-operation +paths: + /users: + get: + security: + - bearerAuth: [] + - apiKey: [] # Alternative auth method +``` + +### Responses(响应) + +可复用的响应定义: + +```yaml +components: + responses: + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: RESOURCE_NOT_FOUND + message: The requested resource was not found + + Unauthorized: + description: Authentication required + headers: + WWW-Authenticate: + schema: + type: string + description: Authentication method + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + ValidationError: + description: Validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: VALIDATION_ERROR + message: Request validation failed + details: + - field: email + code: INVALID_FORMAT + message: Email must be a valid email address + + RateLimitExceeded: + description: Rate limit exceeded + headers: + X-RateLimit-Limit: + schema: + type: integer + description: Request limit per hour + X-RateLimit-Remaining: + schema: + type: integer + description: Remaining requests + X-RateLimit-Reset: + schema: + type: integer + format: int64 + description: Time when limit resets (Unix timestamp) + Retry-After: + schema: + type: integer + description: Seconds to wait before retry + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +``` + +### Examples(示例) + +```yaml +components: + examples: + UserListSuccess: + summary: Successful user list response + value: + data: + - id: 1 + email: john@example.com + name: John Doe + status: active + created_at: "2024-01-15T10:30:00Z" + - id: 2 + email: jane@example.com + name: Jane Smith + status: active + created_at: "2024-01-16T14:20:00Z" + pagination: + offset: 0 + limit: 20 + total: 150 + has_more: true + + CreateUserBasic: + summary: Create user with minimal fields + value: + email: newuser@example.com + name: New User +``` + +## 数据类型 + +### 基本类型 + +```yaml +# String +type: string +example: "Hello World" + +# String with format +type: string +format: email +example: "user@example.com" + +# Integer +type: integer +format: int64 +example: 123 + +# Number (float) +type: number +format: double +example: 99.99 + +# Boolean +type: boolean +example: true + +# Date-time +type: string +format: date-time +example: "2024-01-15T10:30:00Z" + +# Date +type: string +format: date +example: "2024-01-15" + +# UUID +type: string +format: uuid +example: "550e8400-e29b-41d4-a716-446655440000" + +# URI +type: string +format: uri +example: "https://example.com/users/123" +``` + +### 数组 + +```yaml +type: array +items: + type: string +minItems: 1 +maxItems: 10 +uniqueItems: true +example: ["tag1", "tag2", "tag3"] + +# Array of objects +type: array +items: + $ref: '#/components/schemas/User' +``` + +### 对象 + +```yaml +type: object +required: + - name + - email +properties: + name: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 0 + maximum: 120 + +# Additional properties +additionalProperties: false # Strict - no extra properties +additionalProperties: true # Allow any extra properties +additionalProperties: # Extra properties must be strings + type: string +``` + +### 枚举 + +```yaml +type: string +enum: + - active + - inactive + - suspended +default: active +``` + +### OneOf / AnyOf / AllOf + +```yaml +# OneOf - exactly one schema matches +oneOf: + - $ref: '#/components/schemas/CreditCard' + - $ref: '#/components/schemas/BankAccount' + +# AnyOf - one or more schemas match +anyOf: + - $ref: '#/components/schemas/User' + - $ref: '#/components/schemas/Organization' + +# AllOf - all schemas must match (inheritance) +allOf: + - $ref: '#/components/schemas/BaseUser' + - type: object + properties: + admin_level: + type: integer +``` + +## 验证 + +### 字符串验证 + +```yaml +type: string +minLength: 1 +maxLength: 100 +pattern: "^[a-zA-Z0-9_-]+$" +format: email +``` + +### 数字验证 + +```yaml +type: integer +minimum: 0 +maximum: 100 +exclusiveMinimum: true # > 0 instead of >= 0 +multipleOf: 5 +``` + +### 数组验证 + +```yaml +type: array +minItems: 1 +maxItems: 10 +uniqueItems: true +``` + +## 标签 + +将端点组织到逻辑分组中: + +```yaml +tags: + - name: Users + description: User management operations + - name: Orders + description: Order management + - name: Products + description: Product catalog + +paths: + /users: + get: + tags: + - Users +``` + +## 文档 + +### Markdown 支持 + +```yaml +description: | + # User Management + + This endpoint allows you to manage users. + + ## Features + - Create users + - Update profiles + - Delete accounts + + ## Authentication + Requires JWT bearer token. + + ## Example + ```json + { + "name": "John Doe", + "email": "john@example.com" + } + ``` +``` + +## 代码生成 + +从 OpenAPI 规范生成 SDK: + +```bash +# Generate TypeScript client +openapi-generator-cli generate \ + -i openapi.yaml \ + -g typescript-axios \ + -o ./client + +# Generate Python client +openapi-generator-cli generate \ + -i openapi.yaml \ + -g python \ + -o ./python-client + +# Generate server stub +openapi-generator-cli generate \ + -i openapi.yaml \ + -g nodejs-express-server \ + -o ./server +``` + +## 验证工具 + +验证 OpenAPI 规范: + +```bash +# Using Swagger CLI +swagger-cli validate openapi.yaml + +# Using Spectral (advanced linting) +spectral lint openapi.yaml +``` + +## 最佳实践 + +1. **使用 components** —— 复用模式、响应、参数 +2. **添加示例** —— 为所有模式提供真实的示例 +3. **详尽文档** —— 每个端点、参数、响应都应有说明 +4. **对规范做版本管理** —— 跟踪规范的变更 +5. **定期验证** —— 使用工具发现错误 +6. **使用 $ref** —— 引用组件而非重复定义 +7. **包含错误响应** —— 记录所有可能的错误 +8. **添加 operationId** —— 每个操作分配唯一 ID(用于代码生成) +9. **标记端点** —— 组织到逻辑分组中 +10. **提供安全方案** —— 清晰地记录认证方式 diff --git a/references/pagination.md b/references/pagination.md new file mode 100644 index 0000000..79aff62 --- /dev/null +++ b/references/pagination.md @@ -0,0 +1,494 @@ +# 分页模式 + +## 为什么需要分页? + +大型集合不能一次性全部返回,原因在于: +- 性能问题(查询速度慢、数据载荷大) +- 内存限制(服务端和客户端) +- 网络超时 +- 用户体验差 + +集合端点始终要进行分页。 + +## 分页策略 + +### 1. 基于偏移量的分页(Offset-Based Pagination) + +最常见且直观。使用 `offset`(跳过数量)和 `limit`(每页条数)。 + +**请求:** +```http +GET /users?offset=20&limit=10 +``` + +**响应:** +```json +{ + "data": [ + {"id": 21, "name": "User 21"}, + {"id": 22, "name": "User 22"} + ], + "pagination": { + "offset": 20, + "limit": 10, + "total": 150, + "has_more": true + }, + "links": { + "first": "/users?offset=0&limit=10", + "prev": "/users?offset=10&limit=10", + "next": "/users?offset=30&limit=10", + "last": "/users?offset=140&limit=10" + } +} +``` + +**优点:** +- 实现简单 +- 易于理解 +- 支持随机访问(可跳转到任意页) +- 显示总条数 + +**缺点:** +- 偏移量越大性能越差(数据库需要扫描大量行) +- 分页过程中数据发生变化会导致结果不一致 +- 对实时数据效率低下 +- 数据库必须计算总行数(开销大) + +**适用场景:** +- 中小型数据集 +- 数据变更不频繁 +- 需要随机访问页面 +- 需要总条数 + +### 2. 基于页码的分页(Page-Based Pagination) + +使用页码简化的偏移量分页。 + +**请求:** +```http +GET /users?page=3&per_page=10 +``` + +**响应:** +```json +{ + "data": [...], + "pagination": { + "page": 3, + "per_page": 10, + "total_pages": 15, + "total_count": 150 + }, + "links": { + "first": "/users?page=1&per_page=10", + "prev": "/users?page=2&per_page=10", + "next": "/users?page=4&per_page=10", + "last": "/users?page=15&per_page=10" + } +} +``` + +**计算公式:** +- `offset = (page - 1) * per_page` +- `total_pages = ceil(total_count / per_page)` + +**优缺点与基于偏移量的分页相同,但:** +- 对用户更直观(第 1 页、第 2 页) +- 在 Web 应用中常见 + +### 3. 基于游标的分页(Cursor-Based Pagination) + +使用一个不透明的游标(指针)指向下一组结果。 + +**请求:** +```http +GET /users?limit=10 +GET /users?cursor=eyJpZCI6MTIzfQ&limit=10 +``` + +**响应:** +```json +{ + "data": [ + {"id": 21, "name": "User 21"}, + {"id": 22, "name": "User 22"} + ], + "pagination": { + "next_cursor": "eyJpZCI6MzB9", + "prev_cursor": "eyJpZCI6MjB9", + "has_more": true + }, + "links": { + "next": "/users?cursor=eyJpZCI6MzB9&limit=10", + "prev": "/users?cursor=eyJpZCI6MjB9&limit=10" + } +} +``` + +**游标结构(base64 编码):** +```json +{"id": 30, "sort": "created_at"} +``` + +**实现方式:** +```sql +-- 第一页 +SELECT * FROM users ORDER BY created_at DESC LIMIT 10; + +-- 下一页(游标指向最后一项) +SELECT * FROM users +WHERE created_at < '2024-01-15T10:30:00Z' +ORDER BY created_at DESC +LIMIT 10; +``` + +**优点:** +- 结果一致(不会出现跳过或重复项) +- 对大型数据集效率高 +- 适用于实时数据 +- 无需昂贵的 COUNT 查询 +- 数据库性能更优 + +**缺点:** +- 不支持随机访问(无法跳转到第 10 页) +- 不提供总条数 +- 实现较复杂 +- 游标不透明(用户无法修改) + +**适用场景:** +- 大型数据集 +- 数据频繁变更 +- 无限滚动 UI +- 实时信息流 +- 性能至关重要 + +### 4. 键集分页(Keyset Pagination) + +与游标分页类似,但使用实际的字段值而非不透明的游标。 + +**请求:** +```http +GET /users?after_id=20&limit=10 +GET /users?after_created_at=2024-01-15T10:30:00Z&limit=10 +``` + +**响应:** +```json +{ + "data": [ + {"id": 21, "name": "User 21", "created_at": "2024-01-15T11:00:00Z"}, + {"id": 22, "name": "User 22", "created_at": "2024-01-15T11:30:00Z"} + ], + "pagination": { + "after_id": 30, + "limit": 10, + "has_more": true + }, + "links": { + "next": "/users?after_id=30&limit=10" + } +} +``` + +**实现方式:** +```sql +SELECT * FROM users +WHERE id > 20 +ORDER BY id ASC +LIMIT 10; +``` + +**优点:** +- 非常高效(利用索引) +- 游标透明(人类可读) +- 结果一致 +- 实现简单 + +**缺点:** +- 需要已建立索引的列 +- 不支持随机访问 +- 排序仅限于游标字段 +- 多字段排序时较复杂 + +**适用场景:** +- 简单排序(按 ID、时间戳) +- 需要高效分页 +- 希望游标透明 +- 已有合适的索引 + +### 5. 查找分页/基于时间的分页(Seek Pagination / Time-Based) + +针对时序数据专用的键集分页。 + +**请求:** +```http +GET /events?since=2024-01-15T10:00:00Z&until=2024-01-15T11:00:00Z&limit=100 +``` + +**响应:** +```json +{ + "data": [...], + "pagination": { + "since": "2024-01-15T10:00:00Z", + "until": "2024-01-15T11:00:00Z", + "limit": 100, + "has_more": true + }, + "links": { + "next": "/events?since=2024-01-15T11:00:00Z&until=2024-01-15T12:00:00Z&limit=100" + } +} +``` + +**适用场景:** +- 时序数据 +- 日志和事件 +- 活动流 +- 分析数据 + +## 默认限制 + +始终设置合理的默认值和最大限制: + +```json +{ + "default_limit": 20, + "max_limit": 100, + "min_limit": 1 +} +``` + +**校验:** +```http +GET /users?limit=1000 + +响应:400 Bad Request +{ + "error": { + "code": "INVALID_LIMIT", + "message": "Limit must be between 1 and 100. Default is 20." + } +} +``` + +## 响应格式 + +### 标准分页对象 + +```json +{ + "data": [...], + "pagination": { + "limit": 10, + "offset": 20, + "total": 150, + "has_more": true, + "has_previous": true + } +} +``` + +### 链接头部(RFC 5988) + +```http +Link: ; rel="first", + ; rel="prev", + ; rel="next", + ; rel="last" +``` + +**使用方:** GitHub API + +### 嵌入式链接 + +```json +{ + "data": [...], + "_links": { + "self": { "href": "/users?offset=20&limit=10" }, + "first": { "href": "/users?offset=0&limit=10" }, + "prev": { "href": "/users?offset=10&limit=10" }, + "next": { "href": "/users?offset=30&limit=10" }, + "last": { "href": "/users?offset=140&limit=10" } + } +} +``` + +## 分页与排序 + +分页时始终支持排序: + +```http +GET /users?sort=created_at&order=desc&limit=10 +GET /users?sort=-created_at&limit=10 # 降序 +GET /users?sort=last_name,first_name&limit=10 # 多字段 +``` + +**对于游标分页,游标必须包含排序字段:** +```json +{ + "cursor": { + "id": 123, + "created_at": "2024-01-15T10:30:00Z", + "sort_fields": ["created_at", "id"] + } +} +``` + +## 分页与过滤 + +将过滤与分页结合使用: + +```http +GET /users?status=active&role=admin&offset=0&limit=10 +``` + +**重要提示:** 先过滤再分页: +1. 过滤记录 +2. 统计过滤后的结果数 +3. 应用分页 +4. 返回分页后的子集 + +## 总条数 + +### 包含总条数 + +```json +{ + "data": [...], + "pagination": { + "total": 1523, + "limit": 10, + "offset": 20 + } +} +``` + +**优点:** +- 客户端知道结果总数 +- 可计算总页数 +- 更好的用户体验(显示"第 3 页,共 153 页") + +**缺点:** +- COUNT 查询开销大 +- 拖慢响应速度 +- 对大型或频繁变更的数据集不准确 + +### 省略总条数 + +```json +{ + "data": [...], + "pagination": { + "has_more": true, + "limit": 10 + } +} +``` + +**适用场景:** +- 大型数据集(COUNT 太慢) +- 实时数据(总数不断变化) +- 游标分页 +- 无限滚动 UI + +### 可选的总条数 + +让客户端决定是否请求总条数: + +```http +GET /users?limit=10&include_total=true +``` + +## 边界情况 + +### 空结果 + +```json +{ + "data": [], + "pagination": { + "offset": 0, + "limit": 10, + "total": 0, + "has_more": false + } +} +``` + +### 最后一页 + +```json +{ + "data": [{"id": 150, "name": "Last User"}], + "pagination": { + "offset": 140, + "limit": 10, + "total": 150, + "has_more": false + }, + "links": { + "first": "/users?offset=0&limit=10", + "prev": "/users?offset=130&limit=10", + "next": null + } +} +``` + +### 超出范围 + +```http +GET /users?offset=10000&limit=10 + +响应:200 OK(空结果) +{ + "data": [], + "pagination": { + "offset": 10000, + "limit": 10, + "total": 150, + "has_more": false + } +} +``` + +或者对于不存在的页面返回 404: +```http +GET /users?page=1000&per_page=10 + +响应:404 Not Found +{ + "error": { + "code": "PAGE_NOT_FOUND", + "message": "Page 1000 does not exist. Total pages: 15" + } +} +``` + +## 最佳实践 + +1. **始终对集合进行分页**——永远不要返回无限制的列表 +2. **设置合理的默认值**——默认每页 20–50 条 +3. **强制执行最大限制**——防止过载(最大 100–1000 条) +4. **包含 has_more 标志**——告知客户端是否还有更多结果 +5. **提供导航链接**——让获取上一页/下一页变得简单 +6. **文档化分页**——说明游标格式、限制、默认值 +7. **保持一致性**——在所有端点上使用相同的分页模式 +8. **考虑性能**——根据数据大小/类型选择策略 +9. **支持排序**——让客户端控制结果顺序 +10. **处理边界情况**——空结果、最后一页、无效游标 + +## 对比矩阵 + +| 特性 | 偏移量 | 页码 | 游标 | 键集 | +|---------|--------|------|--------|--------| +| 性能 | 偏移量大时差 | 差 | 优秀 | 优秀 | +| 随机访问 | 支持 | 支持 | 不支持 | 不支持 | +| 总条数 | 支持 | 支持 | 不支持 | 可选 | +| 一致性 | 差 | 差 | 优秀 | 优秀 | +| 复杂度 | 简单 | 简单 | 中等 | 中等 | +| 实时数据 | 差 | 差 | 优秀 | 优秀 | +| 数据库负载 | 高 | 高 | 低 | 低 | +| 适用场景 | 小数据集 | Web UI | 信息流/流式数据 | 大型数据集 | diff --git a/references/rest-patterns.md b/references/rest-patterns.md new file mode 100644 index 0000000..9007524 --- /dev/null +++ b/references/rest-patterns.md @@ -0,0 +1,342 @@ +--- +name: rest-design-patterns +description: REST 设计模式参考手册 +metadata: + type: reference +--- + +# REST 设计模式 + +## 面向资源的架构 + +REST API 围绕资源(而非动作)构建。资源是 API 中的名词。 + +### 资源标识 + +**良好的资源 URI:** +``` +GET /users # 集合 +GET /users/{id} # 单个资源 +GET /users/{id}/orders # 嵌套集合 +POST /users # 创建资源 +PUT /users/{id} # 替换资源 +PATCH /users/{id} # 更新资源 +DELETE /users/{id} # 删除资源 +``` + +**不良的资源 URI:** +``` +POST /getUser # URI 中包含动词 +POST /createUser # URI 中包含动词 +GET /user?action=delete # 将动作作为查询参数 +``` + +### 资源命名约定 + +- 集合使用复数名词:`/users`、`/orders`、`/products` +- 使用小写字母和连字符以提高可读性:`/shipping-addresses` +- 避免深层嵌套(最多 2-3 层):`/users/{id}/orders/{orderId}` +- 使用查询参数进行过滤:`/users?status=active&role=admin` + +## HTTP 方法语义 + +### 安全方法与幂等方法 + +| 方法 | 安全 | 幂等 | 使用场景 | +|--------|------|------------|----------| +| GET | 是 | 是 | 获取资源 | +| POST | 否 | 否 | 创建资源、非幂等操作 | +| PUT | 否 | 是 | 替换整个资源 | +| PATCH | 否 | 否 | 部分更新 | +| DELETE | 否 | 是 | 删除资源 | +| HEAD | 是 | 是 | 仅获取元数据 | +| OPTIONS | 是 | 是 | 获取允许的方法 | + +### 方法使用 + +**GET —— 获取资源** +```http +GET /users/123 +Accept: application/json + +响应:200 OK +{ + "id": 123, + "name": "John Doe", + "email": "john@example.com", + "created_at": "2024-01-15T10:30:00Z" +} +``` + +**POST —— 创建资源** +```http +POST /users +Content-Type: application/json + +{ + "name": "Jane Smith", + "email": "jane@example.com" +} + +响应:201 Created +Location: /users/124 +{ + "id": 124, + "name": "Jane Smith", + "email": "jane@example.com", + "created_at": "2024-01-16T14:20:00Z" +} +``` + +**PUT —— 替换资源** +```http +PUT /users/123 +Content-Type: application/json + +{ + "name": "John Doe Updated", + "email": "john.new@example.com" +} + +响应:200 OK +{ + "id": 123, + "name": "John Doe Updated", + "email": "john.new@example.com", + "updated_at": "2024-01-17T09:15:00Z" +} +``` + +**PATCH —— 部分更新** +```http +PATCH /users/123 +Content-Type: application/json + +{ + "email": "john.updated@example.com" +} + +响应:200 OK +{ + "id": 123, + "name": "John Doe", + "email": "john.updated@example.com", + "updated_at": "2024-01-17T10:00:00Z" +} +``` + +**DELETE —— 删除资源** +```http +DELETE /users/123 + +响应:204 No Content +``` + +## HTTP 状态码 + +### 成功状态码(2xx) + +- **200 OK** —— 请求成功(GET、PUT、PATCH) +- **201 Created** —— 资源已创建(POST),需包含 Location 头部 +- **202 Accepted** —— 请求已接受,待异步处理 +- **204 No Content** —— 成功但无响应体(DELETE) + +### 重定向(3xx) + +- **301 Moved Permanently** —— 资源已永久迁移 +- **302 Found** —— 临时重定向 +- **304 Not Modified** —— 缓存版本仍然有效 + +### 客户端错误(4xx) + +- **400 Bad Request** —— 请求语法无效或校验错误 +- **401 Unauthorized** —— 需要认证或认证失败 +- **403 Forbidden** —— 已认证但无权限 +- **404 Not Found** —— 资源不存在 +- **405 Method Not Allowed** —— 该 HTTP 方法不被资源支持 +- **409 Conflict** —— 请求与当前状态冲突(例如重复) +- **422 Unprocessable Entity** —— 语法正确但语义错误 +- **429 Too Many Requests** —— 请求频率超限 + +### 服务端错误(5xx) + +- **500 Internal Server Error** —— 服务器意外错误 +- **502 Bad Gateway** —— 上游服务器返回无效响应 +- **503 Service Unavailable** —— 服务器暂时不可用 +- **504 Gateway Timeout** —— 上游服务器超时 + +## HATEOAS(超媒体) + +### 超媒体驱动的 API + +包含指向相关资源和可用操作的链接: + +```json +{ + "id": 123, + "name": "John Doe", + "email": "john@example.com", + "_links": { + "self": { "href": "/users/123" }, + "orders": { "href": "/users/123/orders" }, + "update": { "href": "/users/123", "method": "PATCH" }, + "delete": { "href": "/users/123", "method": "DELETE" } + } +} +``` + +### HAL(超文本应用语言) + +```json +{ + "id": 123, + "name": "John Doe", + "_links": { + "self": { "href": "/users/123" } + }, + "_embedded": { + "orders": [ + { + "id": 456, + "total": 99.99, + "_links": { + "self": { "href": "/orders/456" } + } + } + ] + } +} +``` + +## 内容协商 + +### Accept 头部 + +```http +GET /users/123 +Accept: application/json + +GET /users/123 +Accept: application/xml + +GET /users/123 +Accept: application/hal+json +``` + +### 响应 Content-Type + +```http +Content-Type: application/json; charset=utf-8 +Content-Type: application/problem+json +Content-Type: application/hal+json +``` + +## 幂等性 + +### 幂等操作 + +**PUT —— 始终幂等:** +多次相同的 PUT 请求与单次请求产生相同的结果。 + +**DELETE —— 幂等:** +第一次 DELETE 返回 204,后续 DELETE 返回 404(最终状态相同)。 + +**POST —— 默认不幂等:** +使用 `Idempotency-Key` 头部实现幂等 POST: + +```http +POST /payments +Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 +Content-Type: application/json + +{ + "amount": 100.00, + "currency": "USD" +} +``` + +服务端存储幂等键,对重复请求返回相同响应。 + +## 缓存控制 + +### 缓存头部 + +```http +Cache-Control: public, max-age=3600 +Cache-Control: private, no-cache +Cache-Control: no-store +ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" +Last-Modified: Wed, 15 Jan 2024 10:30:00 GMT +``` + +### 条件请求 + +```http +GET /users/123 +If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4" + +响应:304 Not Modified +``` + +```http +PUT /users/123 +If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4" +Content-Type: application/json + +{ + "name": "Updated Name" +} + +响应:412 Precondition Failed(如果 ETag 不匹配) +``` + +## URI 模式 + +### 一致的 URI 结构 + +``` +/{version}/{resource} +/{version}/{resource}/{id} +/{version}/{resource}/{id}/{sub-resource} +/{version}/{resource}/{id}/{sub-resource}/{sub-id} +``` + +### 查询参数 + +**过滤:** +``` +GET /users?status=active&role=admin +GET /products?category=electronics&price_min=100&price_max=500 +``` + +**排序:** +``` +GET /users?sort=created_at +GET /users?sort=-created_at # 降序 +GET /users?sort=name,created_at # 多字段 +``` + +**字段选择:** +``` +GET /users?fields=id,name,email +GET /users?exclude=password,social_security_number +``` + +**搜索:** +``` +GET /users?q=john +GET /products?search=laptop +``` + +## 最佳实践 + +1. **使用名词,而非动词** —— 资源是名词,方法是动词 +2. **集合使用复数** —— 使用 `/users` 而非 `/user` +3. **命名保持一致** —— 选择 snake_case 或 camelCase 并坚持使用 +4. **使用正确的状态码** —— 选用恰当的 HTTP 状态码 +5. **包含元数据** —— 在响应中提供分页、过滤、排序信息 +6. **对 API 进行版本控制** —— 从第一天起就规划演进 +7. **记录所有内容** —— OpenAPI 规范、示例、错误码 +8. **默认启用安全** —— HTTPS、认证、速率限制 +9. **支持过滤** —— 让客户端精确获取所需数据 +10. **实现 HATEOAS** —— 使 API 具备自文档和自发现能力 diff --git a/references/versioning.md b/references/versioning.md new file mode 100644 index 0000000..9745684 --- /dev/null +++ b/references/versioning.md @@ -0,0 +1,393 @@ +--- + +# API 版本控制策略 + +## 为什么要对 API 进行版本控制? + +API 版本控制允许你在演进 API 的同时,保持对现有客户端的向后兼容性。破坏性变更需要发布新版本。 + +### 破坏性变更 + +需要发布新版本的变更: +- 删除或重命名字段 +- 更改字段类型(如字符串改为整数) +- 在请求中新增必填字段 +- 更改响应结构 +- 删除端点 +- 更改同一场景下的 HTTP 状态码 +- 更改认证机制 + +### 非破坏性变更 + +不需要发布新版本的安全变更: +- 新增端点 +- 新增可选请求字段 +- 在响应中新增字段(客户端应忽略未知字段) +- 修复 Bug +- 性能优化 +- 为已有资源新增 HTTP 方法 + +## 版本控制策略 + +### 1. URI 版本控制 + +最常见且最直观的方式。版本号作为 URL 路径的一部分。 + +```http +GET /v1/users/123 +GET /v2/users/123 +``` + +**优点:** +- 在 URL 中清晰可见 +- 易于理解与实现 +- 路由和缓存简单 +- 可同时运行多个版本 + +**缺点:** +- 违反 REST 原则(同一资源对应不同 URI) +- 修改版本号需要更新客户端代码 +- 可能导致 URI 泛滥 + +**实现方式:** +``` +/v1/users +/v1/products +/v2/users # 包含破坏性变更的新版本 +/v2/products +``` + +### 2. 标头版本控制 + +版本号通过 HTTP 标头(Accept 标头或自定义标头)指定。 + +**Accept 标头:** +```http +GET /users/123 +Accept: application/vnd.myapi.v1+json + +GET /users/123 +Accept: application/vnd.myapi.v2+json +``` + +**自定义标头:** +```http +GET /users/123 +API-Version: 1 + +GET /users/123 +API-Version: 2 +``` + +**优点:** +- URI 保持稳定 +- 更符合 REST 风格(同一资源对应同一 URI) +- 将版本控制与资源标识分离 + +**缺点:** +- 不够直观(调试难度更大) +- 路由更复杂 +- 在浏览器中测试困难 +- 缓存更复杂 + +### 3. 查询参数版本控制 + +版本号通过查询参数指定。 + +```http +GET /users/123?version=1 +GET /users/123?version=2 + +# 或 +GET /users/123?api-version=1 +GET /users/123?api-version=2 +``` + +**优点:** +- 实现简单 +- 易于测试 +- 在 URL 中可见 + +**缺点:** +- 污染查询字符串 +- 不够语义化(版本不是筛选条件) +- 可能与其他查询参数冲突 + +### 4. 内容协商 + +客户端通过内容协商指定所需的版本。 + +```http +GET /users/123 +Accept: application/vnd.myapi+json; version=1 + +GET /users/123 +Accept: application/vnd.myapi+json; version=2 +``` + +**优点:** +- 非常符合 REST 风格 +- 灵活的内容类型协商 +- URI 稳定 + +**缺点:** +- 实现复杂 +- 对开发者不够直观 +- 测试难度更大 + +## 推荐方案 + +**推荐大多数 API 使用 URI 版本控制**,原因如下: +- 最明确、最易于发现 +- 易于理解和调试 +- 实现和维护简单 +- 版本之间界限清晰 + +``` +/v1/users +/v2/users +/v3/users +``` + +## 版本格式 + +### 仅使用主版本号 + +公开 API 使用简单的主版本号(v1、v2、v3): +``` +/v1/users +/v2/users +``` + +**优点:** +- 简单明了 +- 易于沟通 +- 促使开发者慎重考虑破坏性变更 + +### 基于日期的版本号 + +部分 API 使用日期作为版本号: +``` +/2024-01-01/users +/2024-06-15/users +``` + +**使用者:** Stripe、GitHub API + +**优点:** +- 版本发布时间一目了然 +- 时间线清晰易懂 +- 不会混淆主版本号与次版本号 + +**缺点:** +- 对客户端不够直观 +- 难以了解具体变更内容 + +## 版本生命周期 + +### 1. 引入阶段 + +新版本与旧版本同时发布: +``` +/v1/users # 仍受支持 +/v2/users # 新版本可用 +``` + +发布新版本时需公告: +- 解释变更的博客文章 +- 迁移指南 +- 破坏性变更列表 +- v1 版本的弃用时间表 + +### 2. 弃用阶段 + +将旧版本标记为已弃用,但保持其继续运行: + +```http +GET /v1/users/123 + +响应: +Deprecation: true +Sunset: Wed, 15 Jan 2025 00:00:00 GMT +Link: ; rel="successor-version" + +{ + "id": 123, + "name": "John Doe" +} +``` + +**弃用标头:** +- `Deprecation: true` — 表示该版本已弃用 +- `Sunset: ` — 该版本将于何时移除(RFC 8594) +- `Link: ; rel="successor-version"` — 指向新版本 + +### 3. 退役阶段 + +旧版本在公布的日期正式下线。 + +对已弃用的端点返回 410 Gone: +```http +GET /v1/users/123 + +响应:410 Gone +{ + "error": { + "code": "VERSION_SUNSET", + "message": "API v1 已于 2025-01-15 退役。请使用 v2。", + "documentation_url": "https://api.example.com/docs/migration-v1-to-v2" + } +} +``` + +## 弃用策略 + +### 推荐时间线 + +1. **宣布弃用** — 至少在退役前 6 个月 +2. **支持期** — 两个版本同时运行 6-12 个月 +3. **退役日期** — 提前明确告知具体日期 +4. **宽限期** — 在完全关闭前,提供 30 天的 410 Gone 响应 + +### 沟通渠道 + +- API 响应标头 +- 发送给注册开发者的邮件 +- 博客文章和更新日志 +- 控制台通知 +- 文档更新 +- 状态页面公告 + +## 迁移策略 + +### 提供迁移指南 + +```markdown +# 从 v1 迁移至 v2 + +## 破坏性变更 + +### 用户资源变更 + +**v1:** +```json +{ + "id": 123, + "name": "John Doe", + "email": "john@example.com" +} +``` + +**v2:** +```json +{ + "id": 123, + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com" +} +``` + +**迁移步骤:** +- 将 `name` 字段拆分为 `first_name` 和 `last_name` +- 更新客户端代码以使用新字段 +``` + +### 提供辅助工具 + +- 迁移脚本 +- SDK 更新 +- API 差异对比工具 +- 兼容层(临时) + +## 版本发现 + +### 根端点 + +```http +GET / + +响应: +{ + "versions": { + "v1": { + "status": "deprecated", + "sunset_date": "2025-01-15", + "documentation_url": "https://api.example.com/docs/v1" + }, + "v2": { + "status": "current", + "documentation_url": "https://api.example.com/docs/v2" + }, + "v3": { + "status": "beta", + "documentation_url": "https://api.example.com/docs/v3" + } + } +} +``` + +### 版本信息端点 + +```http +GET /v2/version + +响应: +{ + "version": "v2", + "released": "2024-01-15", + "status": "stable", + "sunset_date": null +} +``` + +## OpenAPI 版本控制 + +### 每个版本独立规范文件 + +``` +openapi-v1.yaml +openapi-v2.yaml +openapi-v3.yaml +``` + +每个规范文件都是完整且独立的。 + +### 单一规范多服务器 + +```yaml +openapi: 3.1.0 +info: + title: My API + version: 2.0.0 +servers: + - url: https://api.example.com/v1 + description: 版本 1(已弃用) + - url: https://api.example.com/v2 + description: 版本 2(当前版本) +``` + +## 最佳实践 + +1. **从第一天起就做版本控制** — 从 /v1 开始,而不是 /api +2. **仅使用主版本号** — 使用 v1、v2、v3(而不是 v1.1、v1.2) +3. **提供较长的弃用期** — 给客户端留出迁移时间(6-12 个月) +4. **沟通要清晰** — 使用标头、文档、邮件 +5. **维护旧版本** — 至少同时支持两个版本 +6. **记录变更** — 提供详细的迁移指南 +7. **使用语义化版本控制** — 用于内部/SDK 版本控制 +8. **永远不要在没有警告的情况下破坏兼容性** — 始终提前公告破坏性变更 +9. **提供辅助工具** — 迁移脚本、更新后的 SDK +10. **监控使用情况** — 追踪哪些版本正在被使用 + +## 反模式 + +避免以下错误: + +- **破坏性变更但不升级版本** — 破坏现有客户端 +- **版本过多** — 维护噩梦(最多保持 2-3 个活跃版本) +- **弃用期过短** — 让开发者感到困扰 +- **没有迁移路径** — 让升级变得痛苦 +- **突然退役** — 毫无预警地破坏生产应用 +- **版本控制策略不一致** — 不同端点使用不同策略 +- **对单个端点进行版本控制** — 应在整个 API 中使用一致的版本