API 契约与前后端联调
前后端联调的痛苦,几乎都来自同一件事:没有契约。后端凭感觉改了个字段名,前端不知道;前端按口头描述写的逻辑,后端实际返回的结构对不上;联调那天才发现两边理解完全不同,互相等、互相改、互相怨。API 契约要解决的就是这个——让接口的形状成为一份双方都认可、可校验、可版本化的协议,而不是聊天记录里的一句话。
为什么需要契约
没有契约时,接口的定义散落在聊天记录、文档、代码三处,而且彼此不一致:
text
口头约定:"返回用户列表,每条有 id 和 name"
后端实际:返回 { userId, userName, ... }
前端预期:[{ id, name }]
联调当天:互相阻塞,谁也动不了契约的价值在于单一事实来源:接口长什么样、字段叫什么、什么类型、什么必填,都由一份明确的描述决定,前后端都以此为准。
| 没有契约 | 有契约 |
|---|---|
| 接口定义在聊天记录里 | 一份机器可读的描述文件 |
| 改了字段没人知道 | 改动触发通知和影响分析 |
| 联调当天才发现对不上 | 各自按契约开发,提前对齐 |
| 出问题互相甩锅 | 以契约为准,对错明确 |
OpenAPI 与类型共享
OpenAPI(前身 Swagger)是事实标准的 REST API 描述格式。一份 OpenAPI 文件既描述了接口,又能直接派生出多种产物:
text
OpenAPI 定义(openapi.yaml)
→ 后端:生成路由骨架、请求校验、mock
→ 前端:生成 TypeScript 类型和请求客户端
→ 文档:自动渲染的 API 文档
→ 测试:契约一致性校验最大的收益是类型从契约自动生成,而不是前端手写一遍、后端手写一遍。手写的两份类型迟早会不一致,自动生成保证它们同源。
ts
// 从 OpenAPI 生成的类型,永远和后端一致
type Order = {
id: string
amount: number
status: 'pending' | 'paid' | 'shipped'
}Mock 与并行开发
契约确定后,前后端不用再互相等。后端还没实现,前端可以基于契约 mock 数据先开发:
text
1. 双方一起定契约(OpenAPI)
2. 前端用 mock server 按契约返回假数据,开发 UI
3. 后端按契约实现接口
4. 联调时前端把 mock 切换成真实接口mock 要基于契约而不是前端自己瞎编——否则 mock 和真实接口又是两套。常用的做法是用专门的 mock 工具直接读 OpenAPI 文件,返回符合 schema 的假数据。
变更通知:契约是会改的
契约不是一次定死,它会演进。关键是要让契约的变更可见、可协商,而不是悄悄改:
| 变更类型 | 影响 | 处理 |
|---|---|---|
| 新增可选字段 | 非破坏性 | 直接加,通知即可 |
| 新增接口 | 非破坏性 | 加,通知 |
| 改字段名/类型 | 破坏性 | 必须协商,前端同意 |
| 删除字段 | 破坏性 | 必须走版本协商或废弃周期 |
破坏性变更最危险。一个看似无害的改动(userId 改成 id),能让前端整个页面崩掉。规则:任何破坏性变更必须走评审,并在 CI 里用契约校验工具拦截,而不是靠人记得。
版本协商
当改动确实是破坏性的,靠版本协商来过渡:
text
策略一:URL 版本
/api/v1/orders → /api/v2/orders
v1 保持可用,新功能进 v2,逐步迁移
策略二:header 版本 / 内容协商
Accept: application/vnd.myapp.v2+json不管哪种策略,核心是旧版本要有明确的废弃周期,给消费者时间迁移,而不是改完就删。同时监控旧版本的调用量,等它趋零再下线。
文档即契约
最好的 API 文档就是契约本身——OpenAPI 文件自动渲染出的文档,永远和实现一致。手写的 wiki 文档最大的问题是它会过期,没人维护,最后没人信。
一个简单的判断标准:如果文档和代码不一致,应该是代码错了(没按契约实现),而不是文档过期了。让契约成为唯一事实,两边都向它对齐。
实现层面,可以在 CI 里加一步契约校验:拿真实接口的响应去匹配 OpenAPI schema,不匹配就失败。这样后端偷偷改了返回结构,CI 会直接报红。
前后端协作流程
把契约驱动串成一条流程:
text
1. 需求评审后,前后端一起定 API 契约(OpenAPI)
2. 契约进版本控制,评审后合并
3. 前端基于契约 mock 开发,后端基于契约实现
4. CI 校验实现与契约一致
5. 联调时切换 mock → 真实接口
6. 后续变更走契约评审 + 变更通知这套流程把"联调当天的互相等待"前置成了"早期的契约对齐"。联调成本越早付出越便宜,越晚越贵。
务实的检查清单
- 是否有一份机器可读的 API 契约作为单一事实来源?
- 前后端的类型是否从契约自动生成,而不是各自手写?
- 前端是否能基于契约 mock 并行开发,不用等后端?
- 破坏性变更是否走评审和通知,而不是悄悄改?
- CI 是否校验实现与契约一致,防止文档和代码漂移?
- 旧版本接口是否有废弃周期和调用量监控,而不是改完就删?