2026 年 7 月 4 日 · 阅读时长 9 分钟

好的 API 设计

原作者:Sean Goedecke。原文:Everything I know about good API design

原文页面未标明可转载授权,本文仅基于原文结构整理中文译述,不构成逐段完整翻译。

现代软件工程的大部分工作都绕不开 API。这里的 API 不只包括公开的 HTTP 接口,也包括内部服务接口、只服务某个前端页面的私有接口、GraphQL 接口,甚至命令行工具暴露出来的非网络接口。

关于 API 设计,很多讨论会滑向过度复杂的问题:什么才是「REST」,HATEOAS 是否值得采用,JSON 和 XML 谁更好。Sean Goedecke 的观点更朴素:好的 API 让使用者少想、少踩坑、少被迫改代码。

API 设计是在熟悉感和长期弹性之间取舍

好的 API 往往很无聊。对 API 维护者来说,它是需要设计和打磨的产品;对 API 使用者来说,它只是完成其他目标的工具。使用者花在理解 API 上的时间越多,留给业务开发的时间就越少。

理想的 API 应该足够熟悉,开发者在读文档之前,就大概知道它会怎么工作。REST 之所以常见,不一定是因为它在技术上最优,而是因为它已经足够熟悉。

但 API 和普通软件系统有一个关键区别:发布之后很难再改。只要有人开始依赖某个字段、结构或语义,维护者的每次修改都可能打断下游系统。这个现实会迫使 API 设计者在两个目标之间取舍:

  • 一方面,接口越简单越好;

  • 另一方面,接口要为未来变化留出空间。

API 设计的很多难点,都来自这两个目标之间的冲突。

不要破坏下游用户

公共 API 的首要责任是不要破坏下游用户。新增字段通常可以接受,因为合理的 JSON consumer 应该忽略未知字段;但删除字段、修改字段类型、移动字段位置,都会让依赖这些字段的代码直接失败。

比如把 user.address 移到 user.details.address,在维护者看来可能只是结构更清晰;对下游来说,这就是一次 breaking change。

这里的原则接近 Linux 维护中的一句口号:WE DO NOT BREAK USERSPACE。API 维护者不应该因为接口看起来不够整洁,或某处历史包袱有点别扭,就要求所有下游使用者付出迁移成本。HTTP 里的 Referer 拼写一直没有改成 Referrer,正是因为兼容性比整洁更重要。

真要改接口,就用版本化,但把它当最后手段

有些情况下,breaking change 的技术收益足够高,维护者可能不得不改。负责任的做法是版本化:同时提供旧版本和新版本,让现有用户继续使用旧接口,新用户或愿意迁移的用户再切到新接口。

常见做法是在 URL 中放版本号,例如 /v1/.../v2/...。也可以像 Stripe 那样通过 header 和账号默认版本来控制。形式不同,目标相同:用户应该能够按自己的节奏升级,而不是被维护者突然打断。

但版本化本身代价很高:

  • 文档更难查,用户必须确认自己看到的是对应版本;

  • 测试、调试、客服支持的表面积会快速扩大;

  • 每个 endpoint 都可能变成多个公开版本;

  • 即使后端有转换层,版本差异也会渗透到核心逻辑里。

因此版本化是必要时的工具,但不应该变成默认策略。更好的做法是在第一次设计时尽量保守,把 breaking change 留到确实无法避免时。

API 的成功主要取决于产品

API 自身不会创造价值。用户调用 OpenAI API,是为了使用模型推理能力;调用 Twilio API,是为了发送短信。没有人会因为一个 API 设计优雅而单独使用它。

如果产品足够有价值,一个难用的 API 也会被大量集成。Facebook、Jira 这类产品的 API 经常被抱怨,但只要业务必须集成它们,开发者仍然会花时间适配。

反过来,如果产品本身没有吸引力,API 再优雅也很难让人采用。API 质量通常是边际因素:当两个产品价值接近时,更好用、更稳定的 API 才会影响选择。

不过,「有没有 API」和「API 好不好」是两件事。对很多技术用户来说,完全没有 API 会直接阻止采购或集成。

产品模型差,API 通常也会差

API 往往会暴露产品的基本资源模型。Jira 的 API 会围绕 issue、project、user 之类资源展开;如果产品内部资源设计混乱,API 很难保持优雅。

UI 可以隐藏很多技术约束,但 API 会把这些约束直接暴露给调用方。比如一个评论系统如果内部用链表存储评论,页面上可以通过额外逻辑拼出看似正常的评论列表;但 API 设计时就会遇到问题:一次请求到底返回多少层评论,超长列表如何取,是否要用后台任务异步拉取。

这类问题会让 API 使用者被迫理解系统内部实现,而这通常是坏 API 的来源。好的 API 应该呈现产品的合理抽象,而不是把实现细节推给调用方。

认证要让起步足够简单

公共 API 应该支持长期有效的 API key。OAuth、短期 token 和更严格的凭证机制在安全上更好,也通常应该支持;但很多集成一开始只是一个简单脚本,API key 是让脚本跑起来的最低摩擦方式。

API 使用者并不一定都是职业工程师。销售、产品经理、学生、业余开发者都可能写一点代码来接入系统。如果一个 API 从第一步开始就要求完成 OAuth handshake,很多潜在用户会卡在门口。

对外 API 的设计不能只面向熟练工程师。降低上手成本,本身就是产品能力的一部分。

幂等性让重试变得安全

请求成功时,调用方知道操作已经完成;请求失败时,情况就复杂得多。422 这类验证错误通常说明操作还没发生,但 500、网络超时或连接中断并不能说明后端到底执行到哪一步。

如果调用方请求创建评论时超时,评论可能已经创建,也可能没有。如果盲目重试,可能会创建两条评论。评论重复还算小事,如果操作是转账、发药或其他高风险行为,重复执行就会变成严重事故。

解决方式是幂等性。常见做法是在请求里带上 idempotency key,后端收到带 key 的创建请求时,先检查这个 key 是否已经处理过:

  • 如果处理过,直接返回之前的结果或不再重复执行;

  • 如果没处理过,执行操作并保存这个 key。

这样调用方可以带着同一个 key 多次重试,而实际操作只会发生一次。

idempotency key 可以存到资源表,也可以存到 Redis 这类 key-value store。高风险场景需要更严格的原子性设计;但对很多已有的非幂等接口来说,补一个可用的幂等层也比完全没有保护好。

并不是所有请求都需要幂等 key。读请求重复执行通常无害;按资源 ID 删除时,资源 ID 本身就能起到类似作用。多数 API 可以把幂等 key 做成可选能力,但支付、资金、医疗等高风险操作应该更严格。

API 必须考虑安全阀和限流

UI 用户受手速限制,API 用户受代码速度驱动。一个在 UI 里很少被连续触发的昂贵流程,放到 API 里可能被循环调用到拖垮后端。

因此公开 API 应该有 rate limit,昂贵操作应该有更紧的限制。系统还应该保留按客户临时关闭 API 的能力,在某个集成异常放大流量时,能先保护后端。

响应里也应该包含限流元数据。例如 X-Limit-RemainingRetry-After 这类 header 可以帮助调用方写出更克制的客户端。服务端给出清晰信号后,也更容易采用严格的限流策略。

大列表默认考虑分页

几乎所有 API 都会返回列表,有些列表可能非常长。直接返回全部记录,会让数据库查询、应用层序列化和网络响应都承受不必要的压力。

最简单的分页方式是 page 或 offset,例如 /tickets?page=2/tickets?offset=20。它实现容易,但在超大数据集上会越来越慢,因为数据库需要先跳过前面的记录。

对可能变大的数据集,cursor-based pagination 更稳妥。客户端拿上一页最后一条记录的 cursor,请求下一页;服务端通过索引定位 cursor 后继续取固定数量的记录。这样无论翻到多后面,查询成本都更稳定。

小数据集可以继续用 page 或 offset;但如果数据未来可能膨胀,最好一开始就采用 cursor。否则等规模问题出现后再迁移,成本会很高。

列表响应里最好直接返回 next_page 或类似字段。不要让客户端自己推导下一页该怎么请求。

昂贵字段默认关闭

如果某些响应字段很贵,就不要默认返回。比如用户订阅状态需要后端再调用另一个服务获取,可以让 /users/:id 默认不返回 subscription,只有请求带上 include_subscriptionincludes: ["subscription"] 时才返回。

GraphQL 的一部分价值也来自这个思路:调用方声明自己需要哪些数据,后端只返回这些数据。

但 Sean 对 GraphQL 保持谨慎,原因主要有三点:

  • 对非工程师和许多工程师来说,GraphQL 的入门门槛明显高于 GET /users/1

  • 允许用户构造任意查询,会增加缓存和边界情况处理难度;

  • 后端实现通常比普通 REST API 更繁琐。

GraphQL 不是不能用,但不应该只因为它灵活就默认采用。很多接口用 REST 加可选字段已经足够。

内部 API 可以宽松一点,但不是不用设计

前面的讨论主要针对公共 API。内部 API 的环境不同:使用者通常是同事,技术能力更接近维护者;breaking change 也更容易协调,因为服务数量更少,维护者往往能直接修改所有调用方。

内部 API 也可以采用更复杂的认证方式,因为使用者和运行环境都更可控。

但内部 API 仍然可能引发事故。关键操作依然需要幂等性,昂贵操作依然需要限流或保护机制。内部不等于可以随意设计,只是迁移和沟通成本更低。

可以带走的原则

  1. API 很难设计,因为它一旦发布就不灵活,但使用时又必须容易理解。

  2. 公共 API 的第一责任是不要破坏下游用户。

  3. 版本化可以帮助做 breaking change,但会带来长期维护和用户迁移成本。

  4. API 成功与否主要取决于产品本身,API 质量通常只是边际因素。

  5. 糟糕的产品模型通常会产生糟糕的 API。

  6. 认证要支持简单 API key,降低第一步集成成本。

  7. 会产生副作用的请求应该支持幂等 key,让重试安全。

  8. API 调用以代码速度发生,因此必须有限流、安全阀和清晰的限流元数据。

  9. 可能变大的列表优先使用 cursor-based pagination。

  10. 昂贵字段应该默认关闭,通过 include 参数按需打开。

  11. GraphQL 能解决一部分灵活性问题,但也会引入复杂度。

  12. 内部 API 的约束不同,但仍然需要认真处理幂等、限流和事故边界。

REST 与 SOAP、JSON 与 XML 的选择,通常不如兼容性、产品模型、幂等性、限流和分页直接影响使用成本。OpenAPI schema 很有用;如果团队更适合用 Markdown 写清楚文档,也可以沿用 Markdown。

CO

我是 Cooper,一名生活在中国的开发者,多年来一直在家远程工作。

主要使用 Go、PHP 和 Rust,从事移动端、Web、跨境支付与资金系统开发。目前主要采用 Vibe Coding 构建产品,并以真实运行结果完成验证与交付。