chore: import zh skill api-designer

This commit is contained in:
wehub-skill-sync
2026-07-13 21:36:01 +08:00
commit 8462bf4550
7 changed files with 2822 additions and 0 deletions
+9
View File
@@ -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 清单整理
- 原作者、版权和许可证信息以上游仓库为准
+219
View File
@@ -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/)
+541
View File
@@ -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` — 标识错误类型的 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** — 导致无法调试
- **未记录的错误** — 客户端不知道会遇到什么
- **信息过多** — 暴露内部实现细节
- **暴露堆栈跟踪** — 安全风险
- **不一致的错误格式** — 不同端点使用不同结构
- **缺少错误码** — 只有人类可读的消息
- **错误的状态码** — 在响应体中返回错误时状态码为 200
- **没有请求 ID** — 导致无法调试
- **未记录的错误** — 客户端不知道会遇到什么
- **信息过多** — 暴露内部实现细节
+824
View File
@@ -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. **提供安全方案** —— 清晰地记录认证方式
+494
View File
@@ -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: </users?offset=0&limit=10>; rel="first",
</users?offset=10&limit=10>; rel="prev",
</users?offset=30&limit=10>; rel="next",
</users?offset=140&limit=10>; 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. **设置合理的默认值**——默认每页 2050 条
3. **强制执行最大限制**——防止过载(最大 100–1000 条)
4. **包含 has_more 标志**——告知客户端是否还有更多结果
5. **提供导航链接**——让获取上一页/下一页变得简单
6. **文档化分页**——说明游标格式、限制、默认值
7. **保持一致性**——在所有端点上使用相同的分页模式
8. **考虑性能**——根据数据大小/类型选择策略
9. **支持排序**——让客户端控制结果顺序
10. **处理边界情况**——空结果、最后一页、无效游标
## 对比矩阵
| 特性 | 偏移量 | 页码 | 游标 | 键集 |
|---------|--------|------|--------|--------|
| 性能 | 偏移量大时差 | 差 | 优秀 | 优秀 |
| 随机访问 | 支持 | 支持 | 不支持 | 不支持 |
| 总条数 | 支持 | 支持 | 不支持 | 可选 |
| 一致性 | 差 | 差 | 优秀 | 优秀 |
| 复杂度 | 简单 | 简单 | 中等 | 中等 |
| 实时数据 | 差 | 差 | 优秀 | 优秀 |
| 数据库负载 | 高 | 高 | 低 | 低 |
| 适用场景 | 小数据集 | Web UI | 信息流/流式数据 | 大型数据集 |
+342
View File
@@ -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 具备自文档和自发现能力
+393
View File
@@ -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: </v2/users/123>; rel="successor-version"
{
"id": 123,
"name": "John Doe"
}
```
**弃用标头:**
- `Deprecation: true` — 表示该版本已弃用
- `Sunset: <date>` — 该版本将于何时移除(RFC 8594)
- `Link: <url>; 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 中使用一致的版本