# 07. API 与契约

如何把 API 设计成一种承诺:schema、错误、idempotency、版本控制、兼容性以及 deprecation policy。

- **Author:** Fedor Chishchin
- **Level:** advanced
- **Reading time:** 约 25 分钟阅读
- **Published:** 2026-05-23
- **Source:** https://fedorthinks.com/zh/learn/software-architect-07-api-contracts

---

## 1. 核心思想

传输方式重要,但 contract 更重要:接收什么、返回什么、可能出现哪些错误、什么操作可以安全重试。

### API 应该乏味到可以预测

> 一份好的 contract 不靠风格让人记住,它清晰、完整,不留下任何危险的歧义。

REST、GraphQL、gRPC 和 WebSocket 各自解决不同的问题。REST 适合 resource-oriented 的操作;GraphQL 让 client 灵活控制返回的形状;gRPC 适合带类型的 service-to-service 调用;WebSocket 适合双向 real-time 通道。

但专业的 API 设计不是从传输层开始的,而是从 schema、状态码、错误、idempotency、rate limits、backwards compatibility 以及 deprecation policy 开始。

### 错误也是 contract 的一部分

> 如果导航只说"没能成功",司机就分不清是没油了、路封了,还是地址输错了。

Client 需要 machine-readable 的错误:错误码、给开发者看的消息、出错的字段、是否可重试、correlation id。一个没有结构的 `400 Bad Request` 会逼着 client 去解析文本或者瞎猜。

错误必须和成功响应一样稳定。如果下游系统基于 `PAYMENT_REQUIRES_ACTION` 构建了行为,你不能明天就把它换成一个随手写的字符串。

## 2. 正例与反例

真实世界总在重复:timeout、retry、双击、webhook 重复投递、worker 重启。

### 正例:幂等的支付创建

> 有人按了两下电梯按钮,电梯不应该来两次又走两次。意图只有一个。

`POST /payments` endpoint 接受一个 `Idempotency-Key`。服务端会保存这个 key、请求指纹以及操作结果。相同请求的重复会返回同一个 payment intent;相同 key 但 body 不同的请求会返回冲突。

这样的 contract 保护了钱、UX 和客服。Client 可以在 timeout 之后安全地 retry。

### 反例:伪装成清理的 breaking change

> 不打招呼就换掉单元门的锁,问题不在于新锁有多好。

团队把 `userId` 改名为 `accountId`,因为"这样更对"。内部 frontend 跟着更新了,外部客户端崩了。没有版本、没有 changelog、没有迁移窗口、没有 deprecation warning。

好的 API 尊重自己已经许下的承诺。新模型可以更好,但迁移路径本身就是这个决策的一部分。

#### 自查

- 哪些错误 client 可以自动处理?
- 重试请求会发生什么?
- 我们怎么在不引入 breaking change 的情况下加字段?
- deprecation policy 写在哪里?

下一模块:[数据模型](./software-architect-08-data-model)。
