关于 api-design
面向 API 设计者和评审者的协议级设计参考,适用于各种语言与框架。内容涵盖 REST 与 GraphQL、错误处理、版本控制、分页和认证,并提供实践建议与质量检查清单。
主要功能
- 01REST 与 GraphQL 的选择指引
- 02资源、HTTP 与 Schema 设计原则
- 03错误格式与版本演进建议
- 04分页及认证授权指导
- 05反模式说明与质量检查清单
适用场景
- 01规划新 API 的协议与接口
- 02审查现有 API 的一致性
- 03制定错误响应与版本策略
- 04为团队建立 API 设计规范
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ krzysztofsurdy/ code-virtuoso --skill api-design安装后在 Claude Code 中输入/api-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-evolution.mdgraphql-patterns.mdrest-patterns.md详细介绍
API 设计
设计一致、可预测且易于演进的 API 的原则和模式。适用于任何语言或框架。重点是协议层面的设计决策,而非实现细节。
将 API 表面视为一种产品:使用者应该能够预测其行为、从错误中恢复,并在不阅读源代码的情况下完成集成。
适用场景
- 从头设计新的公开或内部 API
- 审查现有 API 的一致性和易用性
- 为项目选择 REST 还是 GraphQL
- 规划 API 版本管理或迁移策略
- 定义各服务之间的错误响应契约
- 为团队或组织制定 API 标准
REST 与 GraphQL
| 方面 | REST | GraphQL |
|---|---|---|
| 最适用场景 | CRUD 操作繁多、以资源为中心的领域 | 数据复杂且相互关联,客户端需求各异 |
| 数据获取 | 每个端点的响应结构固定 | 客户端指定所需的确切字段 |
| 过度获取 | 很常见,端点会返回完整资源 | 已消除,客户端只请求所需内容 |
| 获取不足 | 很常见,需要多次往返请求 | 已消除,单个查询即可跨越多个关系 |
| 缓存 | 内置 HTTP 缓存(ETag、Cache-Control) | 需要自定义缓存(规范化存储、持久化查询) |
| 文件上传 | 原生支持 multipart | 需要变通方案(multipart 规范或单独的端点) |
| 实时能力 | Webhook、SSE 或轮询 | 规范内置订阅机制 |
| 工具链成熟度 | 成熟:OpenAPI、Postman、HTTP 客户端 | 持续发展中:Apollo、Relay、GraphiQL |
| 学习曲线 | 较低,基于现有 HTTP 知识 | 较高:模式语言、解析器、查询优化 |
| 错误处理 | HTTP 状态码 + 响应正文 | 已执行的操作通常返回 200,并在响应的 errors 数组中包含错误;请求错误可使用 4xx(GraphQL over HTTP 仍处于草案阶段) |
| 版本管理 | URL 路径、标头或查询参数 | 通过弃用和增量变更演进模式 |
选择 REST 的情况: 领域可映射为资源和 CRUD 操作,需要 HTTP 缓存,或客户端较简单(移动应用、第三方集成)。
选择 GraphQL 的情况: 客户端的数据需求各异,需要聚合多个后端服务,或希望前端与后端之间有强类型契约。
两者都适用。许多系统使用 REST 作为外部/公开 API,并使用 GraphQL 实现内部前后端通信。
REST 设计原则
REST API 将领域建模为资源,并使用 HTTP 语义对其进行操作。
核心规则:
- 资源使用名词,而非动词:
/orders,而不是/getOrders - HTTP 方法充当动词:GET 用于读取,POST 用于创建,PUT 用于替换,PATCH 用于更新,DELETE 用于删除
- URL 用于标识资源;查询参数用于筛选、排序或分页
- 集合使用复数名词:
/users、/users/{id} - 嵌套最多两层:
/users/{id}/orders可以;/users/{id}/orders/{id}/items/{id}/variants不可以 - 有意义地使用 HTTP 状态码。不要所有情况都返回 200
- 通过
Accept和Content-Type标头支持内容协商
HATEOAS(超媒体即应用状态引擎)通过在响应中包含链接来增强可发现性。它适用于公开 API,但对于内部服务往往有些过度:
{
"id": 42,
"status": "shipped",
"_links": {
"self": { "href": "/orders/42" },
"cancel": { "href": "/orders/42/cancel", "method": "POST" },
"customer": { "href": "/customers/7" }
}
}
详细约定请参阅 REST 模式参考。
GraphQL 设计原则
GraphQL API 会公开强类型的架构,供客户端以声明式方式查询。
核心规则:
- 先设计架构:在编写解析器之前定义类型系统
- 类型表示领域概念;字段表示属性和关系
- 查询读取数据,变更写入数据,订阅则流式传输数据
- 使用类型系统实施约束(非空、枚举、输入类型)
- 避免过深的嵌套架构,以免查询成本难以预测
- 通过批处理(dataloader 模式)解决 N+1 问题
- 限制查询深度和复杂度,防止滥用
架构优先示例:
type User {
id: ID!
name: String!
email: String!
orders(first: Int, after: String): OrderConnection!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
createdAt: DateTime!
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
详细约定请参阅 GraphQL 模式参考。
错误处理
一致的错误格式是影响力最大的 API 设计决策之一。使用者应能以编程方式解析错误,而无需检查消息字符串。
RFC 9457 Problem Details 格式(推荐用于 REST;该规范取代了 RFC 7807):
使用 Content-Type: application/problem+json 发送。标准的五个成员(type、title、status、detail、instance)之外的成员均为扩展成员,例如下方的 errors。
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 422,
"detail": "Account balance is $10.00 but the transfer requires $50.00.",
"instance": "/transfers/abc-123",
"errors": [
{
"field": "amount",
"code": "insufficient_funds",
"message": "Transfer amount exceeds available balance"
}
]
}
核心原则:
- 使用机器可读的
type或code。客户端应根据代码分支处理,而不是根据消息 - 包含便于调试的、人类可读的
detail - 为每种错误提供稳定且有文档说明的
typeURI。只有在 HTTP 状态已说明全部信息时,才省略type(或使用about:blank) - 验证失败时返回字段级错误,以便客户端突出显示具体输入
- 使用适当的 HTTP 状态码(REST)或结构化错误类型(GraphQL)
- 生产环境中绝不暴露堆栈跟踪、内部路径或 SQL 查询
- 包含关联 ID/请求 ID,以便跨服务追踪错误
GraphQL 错误约定:
{
"data": { "createOrder": null },
"errors": [
{
"message": "Insufficient funds",
"extensions": {
"code": "INSUFFICIENT_FUNDS",
"field": "amount"
}
}
]
}
版本控制
版本控制策略决定如何发布变更,同时不破坏现有使用者。
| 策略 | 机制 | 优点 | 缺点 |
|---|---|---|---|
| URL 路径 | /v1/users | 明确,易于路由 | URL 冗余,难以弃用 |
| Accept 请求头 | Accept: application/vnd.api+json;version=2 | URL 简洁,符合 HTTP 规范 | 不够显眼,不便于随手测试 |
| 查询参数 | /users?version=2 | 实现简单 | 容易遗漏,缓存处理复杂 |
实用建议:
- URL 路径版本控制最常见,也最容易让使用者理解
- 仅在出现破坏性变更时才提升主版本号
- 优先通过新增字段、新端点等方式对 API 进行增量演进,而不是创建新版本
- 弃用某个版本时,应告知停止使用日期并提供迁移指南
详细策略请参阅 API 演进参考。
分页
每个列表端点都需要分页。游标分页与偏移分页会影响性能、一致性和客户端复杂度。
| 方式 | 工作原理 | 优点 | 缺点 |
|---|---|---|---|
| 偏移量 | ?offset=20&limit=10 | 简单,支持“跳转到第 N 页” | 实时插入/删除时结果不一致,大型表上速度较慢 |
| 游标 | ?after=abc123&limit=10 | 适用于实时数据,规模扩大时仍有良好性能 | 无法跳转到任意页面 |
最佳实践:
- 设置最大页面大小(例如 100)和合理的默认值(例如 20)
- 返回分页元数据:
hasNextPage、hasPreviousPage、totalCount(若计算成本较低) - 如果计算
totalCount的成本较高,则将其设为可选,或返回估算值 - 对信息流、活动流以及任何频繁变化的数据使用游标
- 对管理仪表板、报告以及浏览期间很少变化的数据集使用偏移量
游标分页响应示例:
{
"data": [ ... ],
"pagination": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "eyJpZCI6MX0=",
"endCursor": "eyJpZCI6MTB9"
}
}
身份验证和授权
身份验证用于验证身份(你是谁?)。授权用于验证权限(你能做什么?)。
| 机制 | 使用场景 | 说明 |
|---|---|---|
| API keys | 服务到服务、简单集成 | 易于实现;定期轮换;切勿在客户端代码中暴露 |
| OAuth 2.0 | 第三方访问、委托权限 | 行业标准。对于 SPA/移动应用,使用 Authorization Code + PKCE |
| JWT(Bearer tokens) | 微服务的无状态身份验证 | 仅包含必要的声明;设置较短的有效期;验证签名和声明 |
| Session cookies | 基于浏览器的 Web 应用 | 配合 CSRF 防护使用;设置 Secure、HttpOnly、SameSite 标志 |
最佳实践:
- 始终使用 HTTPS,无一例外
- 在
Authorization: Bearer <token>标头中传递令牌,不要放在查询字符串中 - 实施范围/权限,以进行精细的访问控制
- 缺少或凭据无效时返回
401 Unauthorized,权限不足时返回403 Forbidden - 对身份验证端点实施严格的速率限制,以防止暴力攻击
- 支持令牌刷新流程,避免强制用户重新进行身份验证
常见反模式
| 反模式 | 问题 | 修复方法 |
|---|---|---|
| 过于频繁的 API 调用 | 客户端需要 10 次以上请求才能渲染页面 | 汇总相关数据;考虑使用 GraphQL 或组合端点 |
| 万能端点 | 单个端点通过标志接受差异极大的负载 | 拆分为语义明确、用途专一的端点 |
| 命名不一致 | 混用 snake_case、camelCase、复数和单数 | 选定一种约定,并在整个项目中统一执行 |
| 缺少分页 | 列表端点返回无上限的结果 | 始终对集合进行分页;设置最大页面大小 |
| 不进行版本控制的破坏性变更 | 重命名或删除字段会在无提示的情况下破坏客户端 | 使用版本控制,或仅通过新增内容进行演进 |
| 泄露内部信息 | 在 URL 中暴露数据库列名、自增 ID | 映射为稳定的外部标识符(UUID、slug) |
| 忽略幂等性 | 重试 POST 会创建重复资源 | 为非幂等操作支持幂等性密钥 |
| 所有情况都返回 200 | 错误以 HTTP 200 和错误正文返回 | 使用适当的 HTTP 状态码 |
| 不带时区的时间戳 | 2024-01-15 14:30:00 含义不明确 | 始终使用带时区的 ISO 8601 格式:2024-01-15T14:30:00Z |
质量检查清单
发布或审查 API 之前,请检查以下各项:
- 资源命名保持一致(使用复数名词,URL 中不使用动词)
- HTTP 方法符合语义(GET 是安全的,PUT/DELETE 是幂等的)
- 每个列表端点都已分页,并设有最大页面大小
- 错误响应采用统一格式,并包含机器可读的代码
- 需要进行身份验证,且使用 HTTPS
- 已设置速率限制,并提供适当的标头(
Retry-After,以及RateLimit字段或旧版X-RateLimit-*) - 通过版本控制处理破坏性变更,或通过新增内容进行演进以避免此类变更
- 每个端点都有请求/响应示例
- 时间戳使用带时区的 ISO 8601 格式
- ID 是稳定的外部标识符,而非内部自增 ID
- 已为浏览器客户端配置 CORS(如适用)
- 已为响应启用压缩(gzip/brotli)
- API 文档由唯一可信来源生成(OpenAPI schema、GraphQL introspection)