chore: import zh skill api-designer
This commit is contained in:
@@ -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 清单整理
|
||||
- 原作者、版权和许可证信息以上游仓库为准
|
||||
@@ -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/)
|
||||
@@ -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** — 导致无法调试
|
||||
- **未记录的错误** — 客户端不知道会遇到什么
|
||||
- **信息过多** — 暴露内部实现细节
|
||||
@@ -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. **提供安全方案** —— 清晰地记录认证方式
|
||||
@@ -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. **设置合理的默认值**——默认每页 20–50 条
|
||||
3. **强制执行最大限制**——防止过载(最大 100–1000 条)
|
||||
4. **包含 has_more 标志**——告知客户端是否还有更多结果
|
||||
5. **提供导航链接**——让获取上一页/下一页变得简单
|
||||
6. **文档化分页**——说明游标格式、限制、默认值
|
||||
7. **保持一致性**——在所有端点上使用相同的分页模式
|
||||
8. **考虑性能**——根据数据大小/类型选择策略
|
||||
9. **支持排序**——让客户端控制结果顺序
|
||||
10. **处理边界情况**——空结果、最后一页、无效游标
|
||||
|
||||
## 对比矩阵
|
||||
|
||||
| 特性 | 偏移量 | 页码 | 游标 | 键集 |
|
||||
|---------|--------|------|--------|--------|
|
||||
| 性能 | 偏移量大时差 | 差 | 优秀 | 优秀 |
|
||||
| 随机访问 | 支持 | 支持 | 不支持 | 不支持 |
|
||||
| 总条数 | 支持 | 支持 | 不支持 | 可选 |
|
||||
| 一致性 | 差 | 差 | 优秀 | 优秀 |
|
||||
| 复杂度 | 简单 | 简单 | 中等 | 中等 |
|
||||
| 实时数据 | 差 | 差 | 优秀 | 优秀 |
|
||||
| 数据库负载 | 高 | 高 | 低 | 低 |
|
||||
| 适用场景 | 小数据集 | Web UI | 信息流/流式数据 | 大型数据集 |
|
||||
@@ -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 具备自文档和自发现能力
|
||||
@@ -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 中使用一致的版本
|
||||
Reference in New Issue
Block a user