2.3

查看英文版

2.3 API 与接口设计

概述与动机

API(应用程序编程接口)是一个软件向另一个软件提供能力的契约。它是团队、系统与组织相遇之处,也是出错后代价最高、最难挽回的地方。你可以随意重构内部函数签名。已发布的 API 则不同:它是对你可能永远不会见面的消费者所做的承诺,一旦破坏就会伤及他们。随着组织将单体应用拆分为服务,并向合作伙伴和公众开放能力,API 就成为主要的产品界面,也是主要的集成风险所在。

大型团队中,API 正是让人们能够独立工作的关键。设计良好的接口让你无需与每个消费者协调即可更改内部实现,这正是服务边界存在的意义所在。设计不良的接口则会泄露内部细节,迫使各方步调一致地部署,并将一组服务变成一个”分布式单体”:服务虽然在物理上被拆分,但彼此耦合紧密,以至于必须一起构建和部署。你的 API 设计直接决定了团队能够多大程度上独立行动。

在企业和政府场景中,API 还承载着合规、安全与长期维护方面的义务。公共部门的 API 可能被要求遵循开放标准,保持多年稳定,并服务于你无法直接协调的外部开发者。企业级 API 支撑着具有合同服务水平的合作伙伴集成。这一切都提高了对版本管理纪律、向后兼容性、治理和开发者体验的要求。

核心原则

  • 先设计契约。接口是一项深思熟虑的产品决策,而非实现的副产品。
  • 优先考虑消费者的体验,而非自身的便利。
  • 将向后兼容性视为一项承诺。破坏性变更需要新版本和迁移路径。
  • 让简单的做法就是正确的做法:合理的默认值、可预测的错误、一致的约定。
  • 为失败而设计。幂等性(重复请求与单次请求效果相同)、重试、分页和限流是一等重要的考量因素,而非事后补充。
  • 根据交互方式选择协议风格,而非跟随潮流。
  • 将 API 作为产品来治理,配备负责人、生命周期和文档。

建议

采用 API 优先、契约驱动的方式

在编写实现之前,先定义并评审 API 契约,包括其资源、操作、模式(schema)和错误语义。使用机器可读的规范,以便契约能够自动生成文档、客户端和服务端存根、模拟服务器(mock server)以及验证逻辑。这样一来,消费者就可以在你构建实现的同时开始针对模拟服务进行集成,契约也就成为双方共同测试所依据的唯一真实来源。

审慎选择交互风格

根据交互场景,而非个人偏好,在 REST(表现层状态转移)、GraphQL、gRPC 和事件驱动消息传递之间做出选择。对于面向资源、广泛可互操作、可缓存的接口,使用 REST。当不同客户端需要对一个丰富的数据图进行灵活的聚合读取时,使用 GraphQL。对于内部服务之间高性能、强类型的调用,使用 gRPC。对于异步、解耦的工作流以及状态变更的传播,使用事件驱动消息传递。许多大型系统会同时使用多种风格,各自用在最适合的场景。

有纪律地进行版本管理与弃用

采用明确的版本管理策略和已发布的弃用政策:如何对变更进行分类、旧版本支持多长时间,以及如何通知消费者。在向后兼容的变更(新增可选字段、新增端点)与破坏性变更(删除或重命名字段、更改类型或语义)之间划出清晰的界限。切勿重新赋予现有字段新的含义。给消费者留出重叠的迁移窗口,并提前充分沟通时间表。

使错误语义保持一致且机器可读

返回结构化、可预测的错误:稳定的机器可读错误码、人类可读的消息,以及足以据此采取行动的上下文信息,同时不泄露敏感的内部细节。在所有端点上使用相同的状态语义,以便客户端能够统一处理错误。为消费者可能遇到的每一种错误编写文档。

内置幂等性、分页和限流

通过支持幂等性密钥(idempotency key),使写操作可以安全重试,这样客户端在超时后重试就不会导致重复扣费或重复创建。从第一天起就为每个列表端点提供分页,对于大型或不断变化的数据集,优先使用基于游标(cursor)的分页。应用并记录限流规则,并向客户端返回当前的限流状态,以便它们能够优雅地退避。

治理 API 并投资于开发者体验

将每个 API 都视为一项产品,配备负责人、生命周期和目录条目。建立设计评审或 API 标准委员会,确保各团队之间的接口保持一致。投资于开发者体验:准确的参考文档、快速入门指南、示例、沙箱环境和更新日志。在大型生态系统中,一个能让 API 被发现的门户或目录是必不可少的。

权衡取舍:优缺点

风格适用场景优点缺点
REST / HTTP公共、面向资源的 API普及广泛、可缓存、简单、可互操作过度获取或获取不足;往返次数多;若不加规范则契约松散
GraphQL面向多样化客户端的灵活读取客户端指定查询;单一端点;强类型模式缓存与限流复杂;查询成本风险;服务端复杂度高
gRPC内部高性能调用快速、紧凑、强类型、支持流式传输浏览器支持差;可读性较低;工具链较重
事件驱动异步、解耦的工作流松耦合;可扩展;具备韧性难以推理;最终一致性;运维复杂度高

版本管理策略是在稳定性与维护成本之间做取舍。支持多个旧版本能保护消费者,但也会成倍增加需要维护和测试的代码量。向后兼容性是用自身的自由换取消费者的稳定性,对于被广泛使用的 API 而言,这通常是正确的取舍。宏观来看:一个糟糕的 API 决策所付出的代价,会由每一个消费者在整个接口生命周期内共同承担。因此,在这个边界上投入比几乎任何其他地方都更多的设计精力,是值得的。

与团队讨论的问题

  1. 你们如何将一项变更分类为向后兼容还是破坏性变更,又有哪种自动化检查能在发布前发现一次隐蔽的破坏性变更? 本章划出了一条清晰的界限:新增可选字段和新增端点是安全的,而删除或重命名字段、更改类型,或重新赋予字段新的含义,则会破坏消费者的使用。在一个大型团队中,做出变更的人往往看不到每一个消费者,因此一次”微小”的调整可能悄无声息地破坏你从未接触过的合作伙伴的集成。把具体的信号带到会议上:你们是否针对已发布的规范在 CI 中运行自动化的契约兼容性检查,还是依赖某人记住这条规则。在企业和政府场景中,一次破坏性变更会迫使所有合作伙伴进行协调一致的迁移,甚至可能跨越供应商和管理层的变动,成本会随着消费者数量的增加而增长。请确定分类规则,并接入一个兼容性关卡,使不兼容的变更在构建阶段就失败,而不是在集成阶段才暴露。

  2. 幂等性密钥、分页和限流这些可靠性原语,哪些是每个新端点从第一天起就必须具备的? 本章坚持认为这些都是一等重要的考量因素,因为在一个已经上线的扣费端点上事后补加幂等性密钥,或为一个已经上线的列表补加分页,本身就是一次破坏性变更。大型生态系统会放大这一点:一个在测试中运行良好的端点,在真实数据量下可能会崩溃,而一次非幂等的写操作会把一次网络抖动变成重复扣费。带上证据,说明当前哪些端点缺少这些机制,以及一次重试风暴会造成什么后果。把这些默认设置变成新端点不容商量的要求:每个列表端点都使用游标分页,每个写端点都使用幂等性密钥,限流规则有文档记录并返回当前状态。这样就能把未来一次被迫的迁移,转变为一次性的设计习惯。

  3. 你们是否真的在编写实现之前设计并评审契约,还是接口只是从代码中”泄露”出来的? API 优先的建议要求先有一份经过评审的机器可读规范,用以生成文档、存根和模拟服务,并让消费者在你构建实现的同时就能针对模拟服务进行集成。当契约滞后于实现时,接口就会暴露内部数据库结构,并随着实现的每一次变化而变化,这是本章中最典型的反模式。需要审视的信号是:消费者今天就能针对你的模拟服务开始集成,还是必须等待一个真正运行的后端。对于公共和合作伙伴 API 而言,接口就是产品界面,也是出错代价最高的地方,在契约上花一天时间,能节省数周的支持成本。把契约评审设为开始实现前的必要步骤。

  4. 当两个团队需要暴露相同的能力时,哪种交互风格胜出,又由谁有权拒绝引入第四种协议? 本章建议你根据交互场景来选择 REST、GraphQL、gRPC 或事件驱动消息传递,但在规模化之后,真正的风险在于每个团队都会选择自己偏好的方式,导致消费者在每个端点上面对不同的约定。大型组织会为这种碎片化在客户端库、网关、监控以及每个集成者的认知负担上付出代价()现在他们要学习四种习惯用法,而不是一种。带上目前生产环境中已有协议的清单、每种协议当初被选用来服务的交互场景,以及跨越不止一种协议的消费者。这里存在真实的权衡考量:统一的默认选择能减少混乱蔓延,但僵化的强制规定会把适合 gRPC 的问题硬塞进 REST 的框架里。明确指出由哪个标准机构或架构评审团队拥有例外审批权,因为在企业和政府的技术资产中,风格的泛滥会变成一项永久的集成税,而一旦合作伙伴各自依赖了不同的风格,就很难再逆转。

  5. 我们已发布的弃用政策是什么,我们能否证明自己确实遵守了对外宣布的支持窗口期? 本章将版本管理和弃用视为一种纪律:一份书面政策,规定旧版本存活多久、如何通知消费者,以及他们能获得多长的重叠迁移期。一个无法兑现的承诺比没有承诺更糟糕,因为大型生态系统中包含你从未接触过的消费者,他们会持续调用一个已下线的版本,直到它在生产环境中出错为止。把证据带到讨论中:你们目前维护着多少个存活版本、每个版本的实际使用情况、你们能否看到哪些消费者仍在调用一个已弃用的端点,以及上一次下线公告提前了多久发出。这里存在真实的对立压力:维护成本与消费者稳定性,两者都是真实的考量。对于受合同服务水平约束的企业合作伙伴,以及必须跨越不同政府届期和供应商变动而存续的公共部门 API 而言,支持窗口期是一项可能比制定它的团队存续更久的承诺,因此需要确定由谁来负责,以及在下线之前如何证明这样做是安全的。

  6. 我们如何知道自己的开发者体验是好的,还是仅仅因为 API 对我们自己好用就想当然地这么认为? 本章将每个 API 都视为一项产品,其被采用的程度取决于准确的参考文档、快速入门指南、示例、沙箱环境、更新日志,以及一个可被发现的目录。团队常常把”API 能运行”误当作”API 好用”,而这之间的差距会体现为支持工单、失败的集成,以及那些悄悄放弃的消费者。带上可衡量的信号,而非主观看法:新集成者首次成功调用所花费的时间、每个端点的支持工单量、已发布文档相对于实时契约的过时程度,以及新人能否无需给你的团队发邮件就能通过门户自助完成集成。这里的张力在于,文档和门户需要投入真实的精力,这与交付新功能相互竞争,但在一个大型生态系统中,糟糕的开发者体验会把集成成本一次性转嫁给数百个消费者。在政府场景中,开放 API 服务于你无法协调的外部开发者,透明度往往是强制要求,因此一个好用、文档完善、可被发现的接口是公共问责义务的一部分,而不是锦上添花之举。

行业视角

初创企业。 只有两三名工程师,没有时间讲究繁文缛节,因此要让契约保持轻量但真实可用:一份机器可读的规范,让你的首批设计合作伙伴客户能够在你构建的同时据此集成。暂时不要搭建 API 网关、目录或治理委员会,但要固化两个日后难以补加的习惯:写操作上的幂等性密钥和列表上的游标分页,因为在一个已上线的端点上事后补加它们,是你承担不起的破坏性变更。优先选用一种交互风格(几乎总是 REST),这样在第一年就不会背上协议碎片化的包袱。

小型企业。 没有专职的 API 专家,预算也紧张,因此要依靠能够从规范自动生成文档、模拟服务和客户端存根的工具,让一名通才也能维护接口,而无需深厚的协议专业知识。认真权衡购买与自建:一个现成的网关或 API 管理平台能为你提供限流、密钥和开发者门户,否则你就得自己手工搭建。保持接口面小、约定一致,因为每多一个端点、每一种临时拼凑的错误格式,都是一个精简团队必须永远维护下去的负担。

企业。 在众多自主团队之间,核心问题是如何在不成为瓶颈的前提下保持一致性:一份共享的风格指南、一个 API 标准评审机制、一个让接口可被发现的目录,以及在 CI 中自动化的向后兼容性检查,使一次隐蔽的破坏性变更在构建阶段就失败,而不是在集成阶段才暴露。把每个 API 作为一项产品来治理,配备指定的负责人、生命周期和已发布的弃用政策,并衡量采用率、支持负载和破坏性变更的频率,以保持整个产品组合的健康。在全组织范围内统一交互风格和版本管理规则,因为在这个规模上,碎片化才是代价高昂的默认结果。

政府。 采购规则、开放标准强制要求和公共问责制塑造着每一项选择。公开发布契约,遵循强制要求的开放标准,并提供沙箱环境和参考文档,让你无法直接协调的外部开发者能够自助完成集成。将长期向后兼容性视为一项政策要求,因为集成必须跨越不同的政府届期和供应商变动而存续,并使破坏性变更变得罕见、受到严格治理,且提前很久就予以公告。让 API 及其文档保持足够透明,以经得起公众和审计的审视,并避免使用可能困住未来某届政府的专有格式。

示例

初创企业。 一家种子轮阶段的初创公司在发布首个公共 API 时,在编码之前就先以机器可读规范的形式编写契约,使其两家设计合作伙伴客户能够在后端仍在构建时就针对模拟服务进行集成。即便只有少数几个消费者,它也为扣费端点添加了幂等性密钥,为每个列表添加了游标分页,因为一旦合作伙伴依赖上这个 API 之后再补加这些机制,就意味着一次它承担不起的破坏性变更。提前编写契约花费了一天时间,却节省了数周的支持往返沟通。

企业。 一家大型支付公司向数千家商户开放了一个公共 REST API。每个写端点都接受幂等性密钥,因此一次网络重试永远不会产生重复扣费。每个列表端点都使用游标分页。错误携带稳定的错误码,并记录在公开的参考文档中。一份正式的弃用政策为任何版本都保证了较长的支持窗口期,并提前通知、提供迁移指南。这种纪律是一种竞争优势:集成方相信这个 API 不会在他们不知情的情况下发生破坏。

政府。 一项国家数字服务为公民数据发布了一个开放 API,遵循强制要求的开放标准,并采用 API 优先的设计流程。契约在构建之前就已明确规定并经过评审,发布在一个中央政府 API 目录中,并配有沙箱环境,使无法被逐一协调的第三方开发者能够自行完成集成。长期向后兼容性是一项政策要求,因为集成必须跨越不同的政府届期和供应商变动而存续。因此破坏性变更十分罕见,并受到严格治理。

商业案例:动机、投资回报率与总体拥有成本

良好的 API 设计能够降低集成成本,而集成成本往往是连接系统和引入合作伙伴时最大的开销。有了清晰、稳定、文档完善的 API,消费者可以在几天内完成集成,甚至无需提交一张支持工单。糟糕的 API 则会带来无穷无尽的支持负载、失败的集成以及声誉损失。当 API 本身就是产品时,开发者体验会直接驱动采用率和收入。

最大的隐性成本来自破坏性变更。每一次破坏性变更都会迫使所有消费者()无论是内部团队还是外部合作伙伴()进行协调一致的迁移,总成本会随着消费者数量的增加,以及他们步调一致地完成迁移的难度而增长。提前在契约优先设计、向后兼容性和版本管理纪律上进行投入,可以避免这些代价高昂、波及全组织的迁移事件。在与管理层沟通时,把 API 质量定位为团队自主性、合作伙伴生态增长,以及避免代价高昂的被迫迁移的杠杆点。用集成耗时、支持工单量和破坏性变更频率作为你的证据。

反模式与陷阱

  • 实现优先的 API: 接口会泄露内部数据库结构,并随着实现的每一次变化而变化。
  • 隐蔽的破坏性变更: 在不提升版本号的情况下重新赋予字段新含义,或收紧校验规则,会以不可预测的方式破坏消费者的使用。
  • “话痨”接口: 完成一次逻辑操作需要多次往返的设计,会损害性能和可用性。
  • 约定不一致: 每个端点都自创一套命名、错误格式和分页方式,导致客户端无法泛化处理。
  • 缺少分页或限流: 在测试中运行良好的端点,在真实数据量或负载下崩溃。
  • 非幂等的写操作: 重试会导致重复;一次网络抖动就会破坏数据。
  • 版本泛滥: 存活版本过多且没有弃用机制,导致维护成本成倍增加直至失控。
  • 文档是事后补充: 无文档或过时的参考资料,会把全部集成成本转嫁给消费者。

成熟度模型

  • 第 1 级,启动: API 是实现的副产品,没有共享的约定;接口泄露内部数据库结构;破坏性变更司空见惯、未经公告,且往往是在某个消费者的集成失败后才被发现。
  • 第 2 级,发展: 部分团队遵循基本的 REST 约定,非正式地进行版本管理,手工编写文档,但各团队之间的做法并不一致;幂等性、分页和限流仅出现在部分端点上;消费者仍需逐一摸索每个 API 的特性。
  • 第 3 级,标准化: 契约优先的设计方法(配合机器可读规范)已在全组织范围内形成文档并得到落实;已发布的弃用政策、一致的错误语义,以及强制性的幂等性、游标分页和限流适用于每一个新端点;共享的风格指南和 API 标准评审机制确保各团队之间的接口保持一致。
  • 第 4 级,管理: API 产品组合相对于基线被持续衡量和控制:自动化的向后兼容性检查在 CI 中为每一次变更设置关卡,并且你们会跟踪首次成功调用耗时、每个端点的支持工单量、破坏性变更频率、存活版本数量以及各端点的使用情况,使弃用和设计决策建立在证据而非主观意见之上。每个 API 都是目录中一项受治理的产品,配有指定负责人,一旦某项服务偏离目标,指标就会触发相应行动。
  • 第 5 级,编排: API 战略在全组织范围内持续改进并实现集成;目录、网关、版本管理规则和兼容性关卡作为一个统一系统协同运作;组织根据可衡量的采用率和成本,常态化地淘汰、整合和重新界定接口的范围;交互风格和版本管理标准随着生态系统、合作伙伴和技术的变化而演进,破坏性变更变得罕见且管理良好。

讨论思路

  • 你们如何判断一个内部 API 何时足够稳定,可以对外发布?
  • 在你们的场景中,已弃用版本的合适支持窗口期是多久,又由谁来承担这项成本?
  • 在哪些地方应该用 GraphQL 或 gRPC 取代内部的 REST,在哪些地方它们带来的复杂性会超过其价值?
  • 你们如何在众多自主团队之间落实 API 一致性,同时又不使自己成为瓶颈?
  • 面向 AI 消费的 API 和智能体工具接口应该如何改变你们的设计约定?
  • 有哪些自动化检查能在发布前发现不兼容的向后变更?

关键要点

  • 先设计契约;API 是一项产品,也是一份长期有效的承诺。
  • 向后兼容性保护消费者;破坏性变更需要新版本和迁移路径。
  • 根据交互场景选择 REST、GraphQL、gRPC 或事件,而非跟随潮流。
  • 从第一天起就内置幂等性、分页、限流和一致的错误处理。
  • 将 API 作为产品来治理,配备负责人、目录和优秀的开发者体验。

参考文献与延伸阅读

  • Roy Fielding,Architectural Styles and the Design of Network-based Software Architectures(博士论文)
  • Arnaud Lauret,The Design of Web APIs
  • Mike Amundsen,RESTful Web APIs 与 Design and Build Great Web APIs
  • Sam Newman,Building Microservices
  • OpenAPI Specification;JSON Schema(作为参考标准)
  • Martin Kleppmann,Designing Data-Intensive Applications