什么是 API 契约(API Contracts)?定义、示例与最佳实践

每一次集成失败都有同一个根因故事。A 团队建了一个端点。B 团队消费了它。在"那个字段叫 userId"和"其实它现在叫 user_id 了"之间的某处,生产环境出了岔子,于是两个团队在一间"作战室"里耗了一个下午,争论谁对这个 API 的理解才是对的。

解药不是更好的沟通。而是一份更好的产物:一份 API 契约。一份单一、正式、各方约定一致的定义,说明这个 API 做什么,双方都能据以构建、据以校验,并据以相互约束。

本指南涵盖什么是 API 契约、不同 API 风格所用的格式、契约优先与代码优先开发、API 契约测试如何运作,以及那些让契约长期保持可信的最佳实践。

什么是 API 契约?

API 契约是对一个 API 接口正式、经各方约定的规约。它精确而无歧义地定义:

  • 操作(Operations)——API 暴露的端点、方法、查询或过程。对 REST API 而言,就是路径和 HTTP 动词。对 gRPC,就是服务和 RPC。对事件驱动的 API,就是通道和消息类型。
  • 请求与响应模式(Schemas)——所交换数据的确切形状:字段名、类型、必填与可选、格式以及约束。
  • 错误语义——失败长什么样。存在哪些错误码、它们意味着什么,以及错误响应遵循什么结构。
  • 认证与授权——调用方如何标识自己:API 密钥、OAuth scope、JWT claim、mTLS。
  • 版本与稳定性规则——接口的哪些部分是稳定的、变更如何引入、废弃如何运作,以及提供方承诺了哪些保证(限流、SLA)。

关键词是约定。契约不只是对代码今天碰巧做了什么的描述。它是提供方与其消费方之间的一项承诺:"这就是接口,未经预警我们不会破坏它。"正是这项承诺让独立开发成为可能。前端团队可以在后端尚未写完时据契约构建。合作伙伴无需阅读你的源码即可完成集成。

如果你曾从一个 OpenAPI 文件生成过客户端 SDK、从一份规约 mock 出过一个服务,或因为某个 pull request 破坏了已发布的模式而拒绝它,那你就已经在以 API 契约本来该有的方式使用它了:作为一个接口的真理之源。

API 契约格式:每种 API 风格一种

不存在通用的契约格式,因为不存在通用的 API 风格。每个协议家族都收敛到了自己的规约标准。

REST / HTTP API 用 OpenAPI

OpenAPI(前身为 Swagger)是 HTTP API 的主流契约格式。一份 OpenAPI 文档描述路径、操作、参数、请求体、响应模式、认证方案和服务器——全部用 YAML 或 JSON。

paths:
  /orders/{orderId}:
    get:
      summary: Get an order by ID
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The order
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "404":
          description: Order not found

围绕 OpenAPI 的生态系统才是它真正的强项:可交互的文档查看器、客户端与服务端代码生成器、mock 服务器、校验器和 linter,全都消费同一个文件。

gRPC 用 Protocol Buffers

gRPC API 用 Protocol Buffers 在 .proto 文件中定义。proto 文件就是契约——它定义服务、RPC 方法和强类型消息,客户端和服务端代码都从它生成。

service OrderService {
  rpc GetOrder(GetOrderRequest) returns (Order);
}

message GetOrderRequest {
  string order_id = 1;
}

因为 gRPC 中代码生成是强制的,规约与实现之间的契约漂移,在结构上就比 REST 中更难发生。带编号的字段还编码了一套显式的演进策略:你可以新增字段,但重新编号或挪作他用会破坏兼容性。

GraphQL API 用 GraphQL SDL

GraphQL 把契约内建进了协议本身。模式定义语言(Schema Definition Language,SDL)描述 API 支持的每一个类型、查询、变更(mutation)和订阅,而服务器会强制执行它:不匹配模式的请求会在任何解析器(resolver)运行之前被拒绝。内省(Introspection)意味着消费方总能从活动的 API 获取当前的契约。

事件驱动 API 用 AsyncAPI

异步 API——Kafka topic、RabbitMQ 队列、NATS subject、WebSocket——多年来一直是文档的蛮荒西部。AsyncAPI 把 OpenAPI 的方法移植到事件驱动系统,改变了这一局面。一份 AsyncAPI 文档描述通道、通道上的操作(发送/接收)、消息载荷和 broker 绑定。对于"谁发布了什么、谁又消费了它?"是每日之问的架构而言,一份 AsyncAPI 契约就是"有答案"和"考古工程"之间的区别。

AI 智能体(agent)用 MCP 工具模式

最新的契约类型根本不描述服务到服务的接口。模型上下文协议(Model Context Protocol,MCP)让服务向 AI 智能体暴露工具(tool),而每个工具都带有一个名称、一段描述,以及一份针对其输入的 JSON Schema。那份工具清单是一份货真价实的 API 契约——可以说是一份赌注更高的契约,因为它定义了一个自主智能体被允许对你的系统做什么。我们曾深入撰文论述把 MCP 工具当作 API 契约,以及为什么它们应当享有与你的 REST 端点同等的文档严谨度。

要点是:无论你的 API 风格如何,都存在一种机器可读的契约格式与之对应。现代系统通常一次需要好几种——公共 API 用 REST、内部用 gRPC、事件用 AsyncAPI、智能体用 MCP——这恰恰是为什么契约受益于一个统一的归处,而非五个零散的代码仓库。

契约优先 vs 代码优先开发

契约有两种产生方式,而这个选择塑造你的整个 API 工作流。

契约优先(设计优先)

契约优先开发中,你在写任何实现之前先写规约。OpenAPI 文件或 proto 定义被设计、评审并约定一致——然后提供方和消费方都据它构建,往往是并行进行。

优点:

  • 并行开发。 消费方可以在提供方实现的同时生成客户端、据 mock 构建。无人等待。
  • 设计评审先于代码评审。 在一份 YAML diff 里争论一个字段名,远比重构一个已上线的端点便宜。
  • 一致性。 把契约当作刻意的产物来设计,会让在各 API 间强制执行命名约定、分页模式和错误格式变得自然。
  • 以消费者为中心。 你设计的是消费方需要的接口,而非最容易硬接到你现有数据模型上的接口。

缺点:

  • 更多的前期流程。对一个在内部端点上迭代的两人团队来说,一个正式的设计阶段可能是额外开销。
  • 如果实现不据契约校验,就有漂移风险——你需要工具(校验中间件、CI 检查)让它们保持诚实。

代码优先

代码优先开发中,你先写实现,再从中生成契约——注解、反射或框架内省产出 OpenAPI 文档或 GraphQL 模式。

优点:

  • 小团队的速度。 没有单独的设计步骤;契约总能从代码推导出来。
  • 结构上不会漂移。 生成的规约与实现相符,因为它就来自实现。

缺点:

  • 契约成了副产品而非承诺。代码做什么,API 就是什么——包括那些无意为之的部分。
  • 破坏性变更很容易溜过去,因为没有任何东西强制把接口作为接口来评审。
  • 生成的规约往往平庸:缺少描述、错误文档含糊、没有示例。

你该用哪个?

一条务实的经验法则:一个 API 的消费方越多、你对它们的控制越少,契约优先就越划算。 公共 API、合作伙伴集成,以及不同团队之间的契约,都值得用契约优先的方式对待。一个被同一团队拥有的单个前端所消费的内部端点,可以是代码优先的——只要生成的契约仍然被发布、做版本管理,并被检查破坏性变更。

许多成熟团队最终落在一个混合方案上:为速度而代码优先,再配上契约级别的 CI 闸门(破坏性变更检测、模式 lint),这就给了他们契约优先大部分的安全性。

API 契约测试

一份没有任何东西去验证的契约只是一个愿望。API 契约测试就是自动检查提供方和消费方是否真的符合约定接口的实践。三种技术占主导。

消费者驱动的契约测试

在消费者驱动的契约测试中——由 Pact 推广——每个消费方记录下它所依赖的具体交互:"当我 GET /orders/123 时,我期望得到一个 200,响应体里包含 idstatustotal。"这些记录下来的期望构成一份契约,随后在提供方自己的 CI 流水线中针对它回放。

这种方法的威力在于精确。提供方能精确得知每个消费方实际用了哪些字段。想移除一个字段?契约测试会立刻告诉你是否有任何消费方会因此而坏掉——在你部署之前,而非之后。

CI 中的模式校验

更简单、覆盖更广的技术:校验实现与已发布规约相符。

  • 向服务发起请求,并据 OpenAPI 模式校验响应。
  • 使用校验中间件,拒绝任何不符合契约的响应(在预发布环境里很棒)。
  • 对规约本身进行 lint,检查完整性和风格(Spectral 之类的工具)。

这能廉价而持续地捕捉最常见的失败模式——规约说一套、代码做另一套。

破坏性变更检测

最后,对契约本身做 diff。oasdiff(OpenAPI)、Buf(protobuf)和 GraphQL Inspector 这类工具会把规约的新版本与上一版本做比较,并对每处变更分类:新增的(安全),或破坏性的(移除字段、改变类型、新增必填参数)。把它接进 CI,一处破坏性变更就成了一次需要显式、刻意批准的构建失败——而不是给你的消费方的一个无声惊喜。

如果你从这一节里只做一件事,那就做这件。破坏性变更检测搭起来很便宜,却能捕捉最伤人的那些失败。

为什么 API 契约属于你的架构文档

这是大多数团队都会错过的部分。你可以拥有漂亮的 OpenAPI 文件、严谨的 Pact 套件、以及 CI 里的破坏性变更闸门——却仍然无法回答那个在某物需要变更时最要紧的问题:"谁依赖这份契约?"

代码仓库里的一个契约文件描述了一个接口,但它对接口的上下文只字未提。哪个服务实现了它?哪些服务、前端和合作伙伴消费它?如果我们废弃这个端点,到底什么会坏掉?这种知识通常住在人们的脑子里,这意味着每当有人换了团队,它就退化一分。

这正是架构文档和 API 契约相互需要的地方:

  • 没有架构上下文的契约会无声地变陈旧。 没人会注意到那份描述着去年就被重写的服务的、无主的 openapi.yaml,因为没有任何东西把它和它所描述的系统连起来。
  • 没有契约的架构图则不够精确。 两个方框之间一条标着"REST/JSON"的箭头告诉你存在一种关系,却没告诉你什么东西在上面流动。是契约赋予了那条箭头意义。

C4 模型为这种连接提供了天然的结构:契约挂接到实现和消费它们的容器与组件上(关于这些术语的快速温习,见我们的 C4 模型术语表条目)。API 网关容器承载它的 OpenAPI 契约。内部微服务承载它的 proto 文件。以 Kafka 为中心的服务承载定义其通道的 AsyncAPI 文档。

这正是 Archyl 的 API Contracts 功能的运作方式:你导入 OpenAPI、gRPC、GraphQL、AsyncAPI 或 MCP 契约——从 git 同步而来或直接粘贴——并把它们链接到你架构模型中的 C4 元素上。这些链接是双向的:从一份契约你能看到哪些元素实现和消费它,从图上任何一个元素你都能打开描述其接口的实际规约。当一份契约变更时,你一眼就能看到架构的哪些部分处于波及范围内,而不必从部落知识里重建依赖图景。我们在 API Contracts:你的 API 规约,与你的架构相链接 中详细介绍了这一功能。

无论用什么工具,这条原则都成立:当一份契约存放在它所约束的架构元素旁边、而非一个没人打开的文件夹里时,它最有价值。

API 契约最佳实践:一份清单

契约是一项长寿命的承诺,所以要把它当作承诺来对待:

  • 确立单一真理之源。 每份契约只有一个规范的存放位置。如果规约存在于三个地方,那它就等于存在于零个地方。无论那是一个 git 仓库还是像 Archyl 这样的架构平台,每个人都必须知道权威版本住在哪里。
  • 显式地做版本管理。 给每份契约一个版本,并定义一次版本号变动意味着什么。语义化版本(semantic versioning)很好用:新增性变更升 minor 版本,破坏性变更升 major 版本。
  • 不升 major 版本绝不破坏。 移除字段、改变类型、新增必填参数、收紧校验——全都是破坏性的。它们需要一个新的 major 版本或一个新端点,外加一条迁移路径。
  • 写一份废弃策略并遵守它。 在规约里标记废弃的操作、沟通一个下线日期、给消费方一个现实的窗口(以月计,而非以天计),并在移除前监控其使用情况。
  • 像评审代码变更那样评审契约变更。 一份模式 diff 至少应得到与一份实现 diff 同等的审视——它的消费方更多。
  • 自动化执行。 CI 里的模式校验和破坏性变更检测。人来约定契约;机器来强制执行。
  • 记录错误和认证,而非只有正常路径。 那些 400 和 401 才是消费方花费调试时间的地方。把它们规约清楚。
  • 把契约链接到你的架构。 每份契约都应可追溯到实现它的组件和消费它的组件,从而让影响分析成为一次查询,而非一场调查。

常见问题

API 契约和 API 文档有什么区别?

API 文档是为人而写的:指南、教程、示例、概念解释。API 契约是一份正式、机器可读的规约,人和工具都会消费它——它能生成代码、校验请求、驱动 mock,并让 CI 构建失败。好的文档往往是契约生成的,但契约才是那个有约束力的产物:文档描述 API,契约定义 API。

什么是契约优先开发?

契约优先(或设计优先)开发意味着在实现之前先写好并约定一致 API 规约——OpenAPI 文档、proto 文件或 GraphQL 模式。然后消费方和提供方据同一份约定接口并行构建。它把设计讨论前置、使并行工作成为可能,并让契约成为一项刻意的承诺,而非代码的副产品。

什么是 API 契约测试?

API 契约测试自动验证提供方和消费方是否符合约定接口。它包括消费者驱动的契约测试(Pact 风格,把消费方的期望针对提供方回放)、CI 中的模式校验(检查实现与规约相符),以及破坏性变更检测(对规约版本做 diff,在发布前标记不兼容的变更)。

内部 API 也需要契约吗?

需要——可以说更需要,因为内部 API 变化更快,且受较少的仪式所保护。契约可以更轻量(代码优先生成就行),但它仍应被发布、做版本管理,并被检查破坏性变更。大多数由 API 变更引发的生产事故,都是由内部 API 的变更引发的。


准备好给你的 API 契约在架构内部安个家了吗?探索 Archyl 的 API Contracts 功能——OpenAPI、gRPC、GraphQL、AsyncAPI 和 MCP 契约,与你的 C4 模型相链接。或者继续阅读:API Contracts:你的 API 规约,与你的架构相链接 | 把 MCP 工具当作 API 契约 | 什么是 C4 模型?完整指南