Files
skillhub-052-fullstack-dev/references/api-design.md
T
2026-07-13 21:35:57 +08:00

14 KiB
Raw Blame History

name, description, license, metadata
name description license metadata
fullstack-dev-api-design API 设计模式与最佳实践。适用于创建端点、选择方法/状态码、实现分页或编写 OpenAPI 规范。可避免常见的 REST/GraphQL/gRPC 错误。 MIT
version sources
2.0.0
Microsoft REST API Guidelines
Google API Design Guide
Zalando RESTful API Guidelines
JSON:API Specification
RFC 9457 (Problem Details for HTTP APIs)
RFC 9110 (HTTP Semantics)

API 设计指南

面向后端与全栈工程师的框架无关 API 设计指南。涵盖 10 大类共 50 余条规则,按影响程度排序。覆盖 REST、GraphQL 和 gRPC。

适用范围

使用此技能的场景:

  • 设计新 API 或添加端点
  • 审查 API 拉取请求
  • 在 REST / GraphQL / gRPC 之间做选择
  • 编写 OpenAPI 规范
  • 迁移或对现有 API 进行版本管理

不适用的场景:

  • 框架特定的实现细节(请使用对应框架的技能/文档)
  • 前端数据获取模式(请使用 React Query / SWR 文档)
  • 认证实现细节(请使用认证库的文档)
  • 数据库 schema 设计(→ database-schema-design

所需上下文

在应用此技能前,请收集以下信息:

必填项 可选项
目标消费者(浏览器、移动端、服务) 项目中已有的 API 约定
预期请求量(RPS 预估) 当前的 OpenAPI / Swagger 规范
认证方式(JWT、API Key、OAuth 限流需求
数据模型 / 领域实体 缓存策略

快速开始清单

新建 API 端点?写代码前逐一检查:

  • 资源命名为复数名词/orders,而不是 /getOrders
  • URL 使用短横线命名,请求体字段使用驼峰命名
  • 使用正确的 HTTP 方法GET=读取,POST=创建,PUT=全量替换,PATCH=部分更新,DELETE=删除)
  • 使用正确的状态码201 Created、422 Validation、404 Not Found……)
  • 错误响应遵循 RFC 9457 格式
  • 所有列表端点实现分页(默认 20,最多 100
  • 要求认证Bearer Token,而非查询参数)
  • 响应头中包含请求 IDX-Request-Id
  • 包含限流响应头
  • 端点已在 OpenAPI 规范中记录

快速导航

需要…… 跳转至
命名资源 URL 1. 资源建模
选择 HTTP 方法与状态码 3. HTTP 方法与状态码
格式化错误响应 4. 错误处理
添加分页或过滤 6. 分页与过滤
选择 API 风格(REST vs GraphQL vs gRPC 10. API 风格决策
对现有 API 进行版本管理 7. 版本管理
避免常见错误 反模式清单

1. 资源建模(关键)

核心规则

✅ /users                         — 复数名词
✅ /users/{id}/orders              — 1 层嵌套
✅ /reviews?orderId={oid}          — 使用查询参数展平深层嵌套

❌ /getUsers                       — URL 中包含动词
❌ /user                           — 单数
❌ /users/{uid}/orders/{oid}/items/{iid}/reviews  — 超过 3 层嵌套

最大嵌套层级:2 层。 超过此限制时,提升为顶层资源并通过过滤参数查询。

领域对齐

资源映射到领域概念,而非数据库表:

✅ /checkout-sessions       (领域聚合)
✅ /shipping-labels          (领域概念)

❌ /tbl_order_header          (数据库表泄露)
❌ /join_user_role            (内部 schema 泄露)

2. URL 与命名(关键)

上下文 规范 示例
URL 路径 短横线命名 /order-items
JSON 请求体字段 驼峰命名 { "firstName": "Jane" }
查询参数 驼峰命名或下划线命名(保持一致) ?sortBy=createdAt
请求头 连字符大写命名 X-Request-Id

Python 例外: 如果整个技术栈都是 Python/下划线命名,可以在 JSON 中使用 snake_case——但必须在所有端点间保持一致

✅ GET /users          ❌ GET /users/
✅ GET /reports/annual  ❌ GET /reports/annual.json
✅ POST /users          ❌ POST /users/create

3. HTTP 方法与状态码(关键)

方法语义

方法 语义 幂等 安全 请求体
GET 读取 从不
POST 创建 / 动作 始终
PUT 全量替换 始终
PATCH 部分更新 * 始终
DELETE 删除 极少

状态码快速参考

成功类:

状态码 使用时机 响应体
200 OK GET、PUT、PATCH 成功 资源 / 结果
201 Created POST 创建了资源 已创建的资源 + Location 请求头
202 Accepted 异步操作已启动 任务 ID / 状态 URL
204 No Content DELETE 成功、PUT 无请求体

客户端错误类:

状态码 使用时机 关键区别
400 Bad Request 语法格式错误 无法解析
401 Unauthorized 缺少或无效的认证凭据 "你是谁?"
403 Forbidden 已认证但无权限 "我认识你,但不行"
404 Not Found 资源不存在 也可用于隐藏 403
409 Conflict 重复、版本不匹配 状态冲突
422 Unprocessable 语法正确但验证失败 语义错误
429 Too Many Requests 触发限流 包含 Retry-After

服务端错误类: 500(意外错误)、502(上游失败)、503(过载)、504(上游超时)


4. 错误处理(高优先级)

标准错误格式(RFC 9457

所有错误响应均使用此格式:

{
  "type": "https://api.example.com/errors/insufficient-funds",
  "title": "余额不足",
  "status": 422,
  "detail": "账户余额 $10.00 低于提现金额 $50.00。",
  "instance": "/transactions/txn_abc123",
  "request_id": "req_7f3a8b2c",
  "errors": [
    { "field": "amount", "message": "超出余额", "code": "INSUFFICIENT_BALANCE" }
  ]
}

多语言实现

TypeScriptExpress):

class AppError extends Error {
  constructor(
    public readonly title: string,
    public readonly status: number,
    public readonly detail: string,
    public readonly code: string,
  ) { super(detail); }
}

// 中间件
app.use((err, req, res, next) => {
  if (err instanceof AppError) {
    return res.status(err.status).json({
      type: `https://api.example.com/errors/${err.code}`,
      title: err.title, status: err.status,
      detail: err.detail, request_id: req.id,
    });
  }
  res.status(500).json({ title: '内部错误', status: 500, request_id: req.id });
});

PythonFastAPI):

from fastapi import Request
from fastapi.responses import JSONResponse

class AppError(Exception):
    def __init__(self, title: str, status: int, detail: str, code: str):
        self.title, self.status, self.detail, self.code = title, status, detail, code

@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
    return JSONResponse(status_code=exc.status, content={
        "type": f"https://api.example.com/errors/{exc.code}",
        "title": exc.title, "status": exc.status,
        "detail": exc.detail, "request_id": request.state.request_id,
    })

铁律

✅ 所有错误均返回 RFC 9457 错误格式
✅ 每个错误响应都包含 request_id
✅ 字段级验证错误在 `errors` 数组中返回

❌ 绝不在生产环境暴露堆栈信息
❌ 绝不为错误返回 200 状态码
❌ 绝不静默吞掉错误

5. 认证与授权(高优先级)

✅ Authorization: Bearer eyJhbGci...      (请求头)
❌ GET /users?token=eyJhbGci...            URL——会出现在日志中)

✅ 401 → "你是谁?"  (缺少或无效的凭据)
✅ 403 → "你不能这样做"  (已认证,无权限)
✅ 404 → 隐藏资源存在性  (需要时用来替代 403)

限流响应头(始终包含):

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1625097600
Retry-After: 30

6. 分页与过滤(高优先级)

游标分页 vs 偏移分页

策略 适用场景 优点 缺点
游标分页(推荐) 大型/动态数据集 数据一致,无跳变 无法跳转到第 N 页
偏移分页 小型/稳定数据集、管理后台 简单,支持页面跳转 插入/删除时数据漂移

游标分页响应:

{
  "data": [...],
  "pagination": { "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true }
}

偏移分页响应:

{
  "data": [...],
  "pagination": { "page": 3, "per_page": 20, "total": 256, "total_pages": 13 }
}

始终执行: 默认 20 条,最多 100 条。

标准过滤模式

GET /orders?status=shipped&created_after=2025-01-01&sort=-created_at&fields=id,status
模式 规范
精确匹配 ?status=shipped
范围查询 ?price_gte=10&price_lte=100
日期范围 ?created_after=2025-01-01&created_before=2025-12-31
排序 ?sort=field(升序)、?sort=-field(降序)
稀疏字段 ?fields=id,name,email
搜索 ?q=搜索+关键词

7. 版本管理(中高优先级)

策略 格式 最佳适用
URL 路径(推荐) /v1/users 公开 API
请求头 Api-Version: 2 内部 API
查询参数 ?version=2 遗留系统(避免使用)

非破坏性变更(无需升级版本号): 新增可选的响应字段、新增端点、新增可选参数。

破坏性变更(需新版本): 删除/重命名字段、更改类型、收紧验证规则、删除端点。

弃用响应头:

Sunset: Sat, 01 Mar 2026 00:00:00 GMT
Deprecation: true
Link: <https://api.example.com/v2/users>; rel="successor-version"

8. 请求/响应设计(中优先级)

统一的响应格式

{
  "data": { "id": "ord_123", "status": "pending", "total": 99.50 },
  "meta": { "request_id": "req_abc123", "timestamp": "2025-06-15T10:30:00Z" }
}

关键规则

规则 正确 错误
时间戳 "2025-06-15T10:30:00Z"ISO 8601 "06/15/2025"1718447400
公开 ID UUID "550e8400-..." 自增 42
Null 与缺省(PATCH { "nickname": null } = 清空字段 缺省字段 = 不做更改
HATEOAS(公开 API "links": { "cancel": "/orders/123/cancel" } 无可发现性

9. 文档——OpenAPI(中优先级)

设计优先的工作流程:

1. 编写 OpenAPI 3.1 规范
2. 与相关方审查规范
3. 生成服务端桩代码 + 客户端 SDK
4. 实现处理器
5. 在 CI 中对照规范验证响应

每个端点均需记录:摘要、所有参数、请求体及示例、所有响应码及 schema、认证要求。


10. API 风格决策树

什么样的 API?
│
├─ 浏览器 + 移动端客户端,需要灵活查询
│   └─ GraphQL
│       规则:DataLoader(避免 N+1)、深度限制 ≤7、Relay 分页
│
├─ 标准 CRUD,面向公开消费者,缓存很重要
│   └─ REST(本指南)
│       规则:资源、HTTP 方法、状态码、OpenAPI
│
├─ 服务间通信,高吞吐量,强类型
│   └─ gRPC
│       规则:Protobuf schema、大数据流式传输、截止时间
│
├─ 全栈 TypeScript,同一团队拥有客户端和服务端
│   └─ tRPC
│       规则:共享类型,无需代码生成
│
└─ 实时双向通信
    └─ WebSocket / SSE
        规则:心跳、重连、消息排序

反模式清单

# 不要这样做 应该这样做
1 URL 中包含动词(/getUser HTTP 方法 + 名词资源
2 错误时返回 200 正确的 4xx/5xx 状态码
3 混合使用多种命名风格 每个上下文保持一种规范
4 暴露数据库 ID 使用 UUID 作为公开标识符
5 列表不进行分页 始终分页(默认 20 条)
6 静默吞掉错误 结构化的 RFC 9457 错误
7 Token 放在 URL 查询参数中 Authorization 请求头
8 深层嵌套(3 层以上) 使用查询参数展平
9 破坏性变更不升级版本 保持兼容或使用版本管理
10 不设限流 实现限流并通过请求头告知
11 无请求 ID 每个响应都包含 X-Request-Id
12 生产环境暴露堆栈信息 安全的错误消息 + 内部日志

常见问题

问题 1:"这应该是新资源还是子资源?"

现象: URL 路径不断增长(/users/{id}/orders/{id}/items/{id}/reviews

规则: 如果子实体本身有意义,则提升为顶层资源。如果它只在父级上下文中存在,则保持嵌套(最多 2 层)。

/reviews?orderId=123      ✅  reviews 独立存在)
/orders/{id}/items         ✅  items 属于 orders1 层)

问题 2"PUT 还是 PATCH"

现象: 团队对更新语义无法达成一致。

规则:

  • PUT = 客户端发送完整资源(缺失字段 → 设为默认值/null)
  • PATCH = 客户端只发送发生变化的字段(缺失字段 → 保持不变)
  • 不确定时 → PATCH(更安全,更少意外)

问题 3"400 还是 422"

现象: 验证错误码不一致。

规则:

  • 400 = 完全无法解析请求(格式错误的 JSON、错误的 content-type
  • 422 = 解析成功,但值未通过验证(无效的 email、负数数量)