关于 api-design
用于设计服务 API 合约的架构技能,涵盖协议、分页、重试和版本管理。帮助系统设计者将模糊的接口需求转化为具体且经压力检验的决策。
主要功能
- 01按调用模式选择协议
- 02分页策略建议
- 03幂等写入与重试合约
- 04错误与版本管理策略
- 05压力与容量考量
适用场景
- 01定义新服务的接口
- 02为变化中的数据集选择分页方式
- 03避免写入重试产生重复效果
- 04规划兼容的 API 演进
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ proyecto26/ system-design-skills --skill api-design安装后在 Claude Code 中输入/api-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/deep-dive.mdreferences/providers/aws.mdazure.mdgcp.mdgeneric.md详细介绍
API 设计
定义客户端与服务之间的契约:精确的请求和响应格式、调用方如何分页获取数据、如何安全地重试写入、如何报告错误,以及契约如何演进。如果这一点含糊不清,后续的架构图就只能靠猜(GUIDE #8)——在明确请求、响应和对应的键之前,“NoSQL 盒子”或“用户服务”什么问题也解决不了。
何时使用
服务和数据存储已经确定,现在需要定义接口:客户端发送什么、收到什么、如何获取下一页、如何重试付款而不重复扣款,以及 v1 客户端如何适应 v2 部署。只要有人在没有明确格式的情况下说“获取动态”或“存储帖子”,就该考虑使用此技能。
何时不使用
需求和规模尚未明确时——协议选择取决于读写比和延迟目标,而非个人偏好(→ requirements-scoping、back-of-the-envelope)。不要因为 gRPC、GraphQL 或流式传输听起来更现代就贸然采用;普通的 REST/JSON 端点是满足大多数约束的成本最低的契约,而命名你并不需要的高级协议则是 YAGNI 的危险信号。内部数据访问键和分区设计属于 data-storage;此技能设计与之相对应的外部契约。
先明确需求
- 调用形式 — 请求/响应(CRUD)、双向/实时,还是一次请求 → 多个结果(流式传输)?这决定了协议。
- 读写比和结果集大小 — 这会影响分页方式,以及读取是否需要单独的优化路径(→
back-of-the-envelope)。 - 重试安全性 — 写入能否安全地重复执行?哪些操作天然幂等(PUT/DELETE),哪些不是(用于创建/扣费的 POST)?
- 客户端多样性和迭代频率 — 面向升级较慢、需要严格版本控制的公共第三方,还是与你自己的应用一同发布?
- 延迟和负载预算 — 移动网络/高延迟连接适合紧凑的二进制格式和较少的往返;浏览器则适合可缓存的 HTTP。
可选方案
协议 / 风格(根据调用形式选择)
- 基于 HTTP/JSON 的 REST — 使用标准 HTTP 方法执行资源 CRUD。适合作为面向公众、可缓存且兼容浏览器的 API 默认方案。
- RPC / gRPC(HTTP/2、protobuf) — 类型化方法调用、紧凑的二进制格式、流式传输。适用于对延迟和架构契约有要求的内部服务间通信。
- GraphQL — 客户端在一次查询中精确指定所需字段。适用于许多客户端需要获取同一数据图的不同格式,且 REST 的过度或不足获取会造成问题的场景。
- WebSocket / SSE(流式传输) — 服务器持续向客户端推送数据。适用于服务器必须推送更新的场景(聊天、在线状态、实时动态)——参见下方的轮询层级。
- Webhooks — 事件发生时,服务器调用客户端的 URL。适用于向第三方异步传递事件。
服务器推送层级(客户端需要获取最新数据时)
- 轮询 → 长轮询 → SSE → WebSocket:推送效率依次提高,连接成本也随之增加。从轮询开始;仅当具体指标(更新频率、扇出规模)迫使你升级时才采用更高层级。
分页
- 游标(键集分页) — 基于稳定排序键的不透明令牌。大型或持续变化的数据集默认采用此方式。
- 偏移量/条数 —
?offset=40&limit=20。仅用于较小、基本静态且需要跳转到指定页的列表。
幂等性 — 客户端在不安全的写入请求中发送 Idempotency-Key;服务器对重试请求去重。此技能负责定义键的契约(参见“接口草图”)。
版本控制 — URI(/v2/...)、请求头(Accept: application/vnd.x.v2+json),或采用只增不破坏的方式。优先采用只增不破坏的方式;仅在发生破坏性变更时才保留新版本。
权衡
| 选项 | 解决的问题 | 带来的问题 | 何时更改 |
|---|---|---|---|
| REST/JSON | 通用、可缓存、简单,工具支持广泛 | 过度/不足获取;用于图数据时请求繁杂;类型较弱 | 字段定制带来困扰 → GraphQL;内部延迟较高 → gRPC |
| gRPC/RPC | 紧凑、类型明确、快速,原生支持流式传输 | 非浏览器原生;需要代理;对 HTTP 缓存不透明 | 公共/浏览器客户端需要 → REST 网关 |
| GraphQL | 一次往返,客户端选择字段 | 缓存/限流困难;查询开销大(N+1);服务端复杂 | 形态固定且很少(REST 更简单),或查询成本不受限 |
| WebSocket/SSE | 真正的服务端推送、低延迟更新 | 有状态连接,扩展/负载均衡更困难,需要重连逻辑 | 更新不频繁 → 长轮询;仅单向通信 → SSE |
| 游标分页 | 插入数据时仍保持稳定;任意深度下每页均为 O(1) | 令牌不透明;无法随机跳页;需要排序键 | 用户必须跳转到静态数据集的第 N 页 → 偏移分页 |
| 偏移分页 | 简单;可任意跳页 | 插入时数据漂移/重复;较大的偏移量需要扫描,速度较慢 | 数据集增长或变化 → 游标分页 |
| 幂等键 | 可安全重试;不会重复扣费 | 服务端必须存储键并去重;需定义键的 TTL/作用范围 | 操作本身具有幂等性(PUT/DELETE)→ 可考虑跳过 |
| URI 版本控制 | 明确、缓存/日志可见、易于路由 | 版本泛滥;客户端永久固定在某个版本 | 变更为增量式 → 无需新版本 |
压力下的行为
契约决定客户端会在多大程度上放大故障。
- 重试风暴。 超时的非幂等写入会被重试,可能导致重复执行;客户端同步重试会让恢复中的服务遭遇请求洪峰。
幂等键可确保重试安全;负责通过退避/抖动来错开重试的是
resilience-failure。提供429+Retry-After,让行为良好的客户端放慢速度。 - 深度分页。 大偏移量下的偏移分页会迫使存储扫描并丢弃行——一个看似开销很低的端点,在爬虫访问时会变成全表扫描。 游标分页可确保每页的开销始终为 O(页大小)。
- 无界响应。 没有默认页大小、没有最大负载限制,或 GraphQL 查询产生大量扇出 → 单个请求就能耗尽内存/CPU。在契约中限制页大小、深度和复杂度。
- 连接耗尽。 WebSocket/SSE 会为每个客户端占用一个连接;部署后发生的重连风暴可能耗尽文件描述符和负载均衡器槽位。规划带抖动的重连策略,并设置连接数限制。
- 版本变更导致故障。 对共享数据结构进行非增量式变更会同时破坏所有客户端——这是最严重的自我引发故障。采用增量式变更;通过版本进行弃用。
监控: 按状态类别(4xx 与 5xx)统计错误率、各端点的 p99 延迟、重试/幂等重放率、分页深度分布、打开的连接数,以及各版本的流量(以了解何时可以停用旧版本)。
如何应用
- 明确输入。 确定调用形式、读写比例、结果集大小、重试安全性、客户端多样性,以及延迟和负载预算(参见 先明确)。了解这些信息前,不制定契约。
- 根据权衡表选择协议和分页方式。 默认使用 REST/JSON;只有输入条件要求时,才升级到 gRPC(内部延迟和类型安全)、GraphQL(字段形态多样)或推送层级(服务端必须主动推送)。默认使用游标分页;仅对较小、静态且需要跳转到指定页面的集合使用偏移分页。
- 设置关键参数。 根据安全性选择幂等动词(GET/PUT/DELETE 可安全重试,POST 不可),定义
Idempotency-Key契约及其 TTL,固定稳定的错误封装,并选择版本策略(默认采用增量式变更)。 - 对契约进行压力测试。 限制默认和最大页面大小、GraphQL 深度/复杂度及负载大小;为推送制定带随机抖动的重连方案和连接数限制;返回
429+Retry-After。确认 v1 客户端在部署 v2 后仍能正常运行。 - 用数字估算规模。 估算每个端点的请求数/秒、响应大小、每个会话的页面数,以及更新频率 × 扇出;用这些数据确认协议选择和轮询与推送的取舍(→
back-of-the-envelope)。 - 选择提供商。 除非用户指定云服务,否则沿用通用方案;若用户指定了云服务,则映射到其托管网关(参见 选择提供商)。
- 在答案中展示权衡——提供一张
Decision | Solves | Worsens | Change it when…表格,每项选择占一行(例如,所选方案、存储、故障策略);最后一列填写具体触发条件(一个数字或可观测信号)。然后指出最先会出现故障的环节。
应做和不应做
应做
- 默认使用 REST/JSON 和游标分页;只有数字指标要求时才升级。
- 根据重试安全性选择动词,确保重试从设计上就是正确的。
- 要求每个非幂等写入都携带
Idempotency-Key,并限制其 TTL。 - 限制页面大小、查询深度/复杂度和负载,确保最坏情况下的成本可控。
- 在每个端点中保持统一、稳定的错误封装(code、message、request_id、retryable)。
不应做
- 仅仅因为 gRPC、GraphQL 或流式传输听起来很现代就采用它们(这是 YAGNI 的警示信号)。
- 不要对不断增长或不断变化的集合使用偏移分页——深层偏移会扫描存储。
- 不要在共享数据结构上发布非增量式变更,却不引入新版本。
- 不要让响应大小不受限(没有默认页面大小,也没有最大负载大小)。
- 不要在此处重复讲解重试/退避或分片——链接到
resilience-failure/data-storage。
重要数字
选择前先估算:每个端点的请求数/秒、平均和最大响应大小、页面大小 × 每个会话的页面数(影响读取负载),以及更新频率 × 扇出(决定轮询还是推送)。以下几条经验法则可能改变选择:二进制协议(protobuf)通常能将负载缩小数倍;在数据上网线前进行压缩还能进一步缩小负载——这两者在移动网络或高延迟链路上最为重要,因为此时往返延迟占主导。对 N 个客户端以 T 为间隔轮询,每秒会产生 N/T 个几乎总是空的响应请求;当这个数接近推送层级的连接数预算时,应切换到 SSE/WebSocket。限制页面大小(以及 GraphQL 深度/复杂度),确保最坏情况下的响应成本可控;同时限制幂等键的存储窗口(例如 24h),避免去重表无限增长。规范的延迟/QPS/负载参考数据请查看 back-of-the-envelope——不要在此处重复其表格。
接口示例
明确契约。一个使用游标分页和错误封装的读取示例:
GET /v1/users/{id}/posts?limit=20&cursor=eyJ0cyI6MTciLCJpZCI6Ijk5In0
200 OK
{ "data": [ { "id": "p_881", "created_at": "...", "text": "..." } ],
"next_cursor": "eyJ0cyI6...", // null when no more pages
"has_more": true }
# error envelope — stable shape across every endpoint
4xx/5xx
{ "error": { "code": "rate_limited", "message": "…", "request_id": "req_…",
"retryable": true } }
一个幂等写入示例(本技能负责的契约):
POST /v1/payments
Idempotency-Key: 9f1c-… (client-generated UUID, unique per logical operation)
{ "amount": 4200, "currency": "usd", "source": "card_…" }
# Server: first request with a key → execute, store (key → response) for a TTL.
# Retry with same key → return the stored response, do NOT re-execute.
# Same key + different body → 422 (key reuse conflict).
让响应结构与访问模式相匹配:游标应编码存储分页所用的分区/排序键(PK/SK 设计由 data-storage 负责)。根据安全性选择动词——GET(安全)、PUT/DELETE(幂等)、POST(非幂等),确保重试从设计上就是正确的。
选择提供商
默认采用上述通用方案。如果用户指定了云服务商,请阅读
references/providers/<provider>.md,了解托管服务映射、
配额/限制和提供商特定的权衡。如果该提供商没有对应文件,则采用通用方案。
图示
要将请求/响应路径、游标分页循环或幂等重试序列(客户端 → 网关 → 服务 → 存储,其中包含重放分支)可视化,请使用插件内的 architecture-diagram 技能——不要在此处嵌入 Mermaid。快速推理时,可以内联使用一行 ASCII 草图。
相关构建模块
data-storage— 依赖于它:请求/响应和游标的形状与其主键和访问模式相对应(分片/分区在该模块中介绍);请一并设计。resilience-failure— 与它配合使用:该模块负责重试/退避/抖动、超时和速率限制,而幂等键(由此模块负责)则确保这些重试安全。consistency-coordination— 当重试或并发写入不得违反不变量时,与它配合使用;CAP/仲裁理论在该模块中介绍。messaging-streaming— 当契约是异步事件或 webhook 时,可作为同步请求/响应的替代方案。system-design— 为此提供输入:编排器在接口步骤中路由到此处。
参考资料
references/deep-dive.md— 协议机制(HTTP 动词/状态码规范、gRPC 流模式、GraphQL 查询成本限制)、游标令牌构造、完整的幂等键状态机、版本管理/弃用策略和错误契约设计。详细设计契约时请阅读。references/providers/{generic,aws,azure,gcp}.md— API 网关/托管端点映射、会影响决策的限制,以及各环境中的注意事项。