Software Architect · 模块 07

API 是系统之间的一份承诺。一份好的 contract 会比客户端、团队和好几代实现都活得更久。

REST · GraphQL · gRPC · idempotency · compatibility

§ 01

传输方式重要,但 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 构建了行为,你不能明天就把它换成一个随手写的字符串。

§ 02

真实世界总在重复: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 写在哪里?