作为开发者,我们每天都在跟各种各样的 API 打交道,不论是调用 OpenAI 的接口,还是使用短信服务,API 就像软件世界的数字神经系统,是各种服务和数据的重要管道。
不过,不同的 API,给开发者的实际体验,差距也许是巨大的。
如何设计出一套「好」的 API?这个问题似乎和「如何设计好的软件系统」一样,充满了花哨的理论和复杂的范式。
但实际上,API 的设计,远不止技术选型与功能拼装。在 Sean Goedecke 看来,API 设计是一门在灵活性与稳定性、简洁性与前瞻性之间权衡的工程艺术。

这篇文章基于 Sean 的实践体会,并结合大厂的开发经验,讲述 API 接口背后的设计哲学。建议先收藏,再细读。看完之后,你对 API 的理解肯定会再上一个台阶。
好的系统设计,往往是「无聊」的;好用的 API,同样也是「无聊」的。
这里的「无聊」不是功能贫乏,而是行为可预期、命名合乎常识、边界清晰明确,熟悉到几乎没有学习门槛。

对使用者来说,API 只是达成目标的工具;他们花在理解 API 本身的额外精力,基本都算浪费。最理想的状态是:开发者在没打开文档之前,也能凭直觉八九不离十地写出第一段调用代码。
这种「直觉」来自于熟悉的通用设计模式,能最大程度地降低开发者的学习成本。
但另一个现实更棘手:API 一旦发布,变更成本极高。任何破坏性的变更都可能触发大量下游应用服务的宕机,没有哪个开发者愿意使用频繁变更的 API 接口。
这就给 API 的设计提出了挑战:一方面,要尽可能简单、直接、符合直觉;另一方面,为了应对可能得需求变化,又需要通过巧妙的设计来保持灵活性。
因此,API 设计的根本目的和最终挑战,就是要在「上手容易」与「可持续演进」之间找到一个平衡点。
WE DO NOT BREAK USERSPACE.
这是一句来自 Linux 内核社区的名言。意思很直白:新版本不能把老的用户程序(或下游集成)直接整崩。现代软件生态是一个层层依赖的巨大网络,上游一次不经意的调整,可能引发多米诺效应,让成百上千个服务同时出问题。

这也应该成为 API 构建者的铁律。
那么,什么算破坏性变更?
一般而言,增加性的变更是安全的。比如在返回的 JSON 数据中新增某个字段。除非对 API 的解析过分严格,否则设计合理的客户端会忽略那些未知的字段。
但是,下列行为是绝对禁止的:
123 改成字符串 "user-123"。user.address 挪到 user.details.address。不要因为「看起来更整洁」或「当初设计有点别扭」就直接对 API 进行破坏性变更。
一个经典的例子是 HTTP 头里 Referer 的拼写,这其实是单词 referrer 的错误拼写。但几十年过去了,这个错误并没有被修正。原因就是不能轻易破坏既有生态——不破坏用户空间是一种承诺。
在极少的情况下,如果确有必要做出破坏性变更时,版本化是合理的路径。提供 API 版本控制意味着同时支持新旧两个版本的 API 服务:
常见做法是在 URL 中带版本号(如 /v1/...)。比如 OpenAI 的 Chat Completions 暂挂在 v1/chat/completions;如果未来需要大改,可以在 v2/... 下重构,同时保持 v1 可用。
另一种是 Stripe 的做法:通过请求头控制版本(如 Stripe-Version: 2024-04-10),并允许在后台设置账户的默认 API 版本。
无论 URL 路径还是请求头,目标是一致的:让使用者有安全感,能按自己的节奏升级服务。
尽管版本控制是行业通用的做法,但 Sean 认为「版本控制」其实是「必要之恶」:它虽然能兜底,但也会带来巨大的复杂度:
成熟的 API 实现会在后端加一个翻译层,把统一的内部模型映射到不同版本的外部协议格式。然而抽象总会泄漏:一些差异最终还是要落到核心逻辑里处理条件分支。

所以如果不到万不得已,尽量不要新增 API 版本。
在深入探讨具体的 API 设计技巧之前,我们需要先理解一件事情:API 是否成功,完全取决于产品。
换句话说:API 本身不创造价值,它只是连接价值的桥梁。
没有人会因为「接口设计优雅」而选择某个产品。如果产品价值够大,即便 API 不好用,大家也会硬着头皮接入。Facebook、Jira 的 API 在开发者社区里名声不算好,但如果想用它们的能力,你终究得适配它们的规则。
这并不是在否认 API 设计的意义——一个顺手的 API 能显著降低集成成本、提升开发者体验,并在势均力敌的竞争中提供边际优势。
但请记住:API 的采用前提始终是产品本身的价值。
反过来,一个技术上很糟糕的产品,很难产出优雅的 API,因为 API 通常是对核心资源与业务关系的直接映射。当这些资源的内部实现本身就很笨拙时,API 自然也会受到影响。
你应该允许用户使用长生命周期的 API Key 来访问 API,尽管 API key 的安全性不如各种 OAuth 瓶颈。
几乎所有 API 集成都是从一个小脚本起步,而 API Key 是启动门槛最低的方案,首要目标是让开发者尽可能简单地开始使用你的产品。
更重要的是,尽管 API 需要通过代码访问调用,但 API 的用户不一定都是软件工程师——也可能是销售、产品、学生或爱好者。要求他们一上来就走完整套 OAuth 流程,只会把人挡在门外。
建议:
当一个 API 请求成功时,很容易知道它已经完成了任务。但当它失败时呢?
422 多半表示校验未过,没有执行实际操作;500 或超时则很模糊——请求可能已经被执行,只是响应途中失败。这在支付等高风险写操作里尤其致命。

幂等性的目标是:同一语义的请求可安全地重试,而不会产生重复执行的效果。最简单做法是给写请求附带一个幂等键(如 Idempotency-Key),由客户端生成唯一值(比如 UUID)。
当服务端处理时:
这样,客户端遇到超时或 500 时,即便将带同一幂等键的请求重试任意次,都不会产生「重复执行」的副作用。
实现上,用 Redis/数据库小表就够用了,设置合理的 TTL(例如数小时)就能覆盖绝大多数重试窗口。
哪些请求需要幂等?
PUT 倾向于幂等,PATCH 视语义而定(如自增计数就不是)。不过,Sean 也建议,在大多数情况下,幂等性应该是可选的,不要因为概念负担挡住更多人上手。
用户通过 UI 操作受限于手速;但 API 则可以以运行代码的速度被疯狂调用。
因此,要特别小心单次请求中会执行大量工作的 API。
Sean 分享了他在 Zendesk 工作时的一个经历:有个 API 能向某应用的所有用户广播通知,结果有第三方拿它做了「应用内聊天」,每条消息都群发通知。结果对于拥有大量活跃用户的账户,这种调用方法直接把后端服务器搞垮了。
我们没有预料到人们会用这个 API 来构建聊天应用。但实际上,一旦 API 发布出去,人们就会用它做任何他们想做的事情。
现实里,很多事故都来自客户自己在脚本里的「创造性用法」:
应对策略:
X-RateLimit-Limit、X-RateLimit-Remaining、Retry-After 等,帮助客户端实现更合理的调用节奏。长列表是不可避免的。一次性返回所有数据既不现实,也不安全。因此,API 必须分页。
最直观的做法是偏移量分页(offset/limit),但这种做法在数据量大时会出现严重性能问题:数据库为 OFFSET 100000 之类的查询需要从头跳过十万行,越到后面越慢。

更靠谱的是基于游标(cursor-based)分页:客户端拿到第一页后,记住最后一条记录的标识(如 ID=32),下一页请求带上 cursor=32&limit=10。服务端可直接使用索引执行 WHERE id > 32 ORDER BY id LIMIT 10。这种方式无论在第 1 页还是第 10 万页,性能都稳定。
建议:
next_page/next_cursor,减少客户端手动拼装下一页请求的心智负担。那些计算昂贵的字段应默认关闭、按需开启。
例如 /users/:id 默认不返回订阅信息;当请求带 ?include=subscription 时才去做额外调用。
更通用的做法是使用 includes[]=posts&includes[]=subscription 这样的数组参数。
这正式 GraphQL 的理念:客户端声明所需字段,服务端一次性组装返回,而不是像 REST 那样通过访问不同的端点来获取不同资源。

但也要警惕三点:
根据 Sean 的经验,尽管在某些确实需要高度自由查询的场景下,GraphQL 带来的灵活性足以抵消其成本。但在大多数场景里,按需加载已足够灵活。
上面的讨论都是针对外部 API,公司内部使用的 API 则有所不同:
但 API 设计的底线应该是不变的:幂等性、限流、熔断同样重要。如果不加以限制,内部 API 一样可能成为事故源头。
最后,Sean 将他关于 API 设计的核心思想总结为以下几点:
归根到底,优秀的 API 设计是一种务实的、以用户为中心的工程实践,是在简单与灵活、当前与未来之间不断权衡的艺术。
而那些具体的工具与范式反而是次要的;理解背后的取舍与原则,才是让 API 长期好用、被开发者喜爱的根本。