Skip to content

02 · API 设计:REST / gRPC / GraphQL 与版本兼容 ​

速览卡 ​

项内容
一句话定位前端天天在调 API,本篇从服务端一侧讲清楚"为什么要这样设计"
核心内容三种风格的适用边界、资源与状态码约定、破坏性变更怎么处理、分页与错误体设计
读完能回答什么时候该用 gRPC?接口要改字段了怎么不炸客户端?
相关板块01 · 运行时选型 | 03 · 认证与授权 | 04 · 浏览器与网络

一、三种风格:按"谁在调用"来选 ​

不要按流行度选,按调用方选。

风格适用不适用传输
REST对外开放 API、第三方集成、前后端分离内部高频调用、强类型诉求HTTP + JSON
gRPC服务间内部调用、高性能、强契约浏览器直连(需 grpc-web)、对外开放HTTP/2 + Protobuf
GraphQL客户端形态多、字段需求差异大(多端)简单 CRUD、缓存与监控体系薄弱时HTTP + JSON

最常见的正确组合:对外 REST,对内 gRPC。 这不是折中,而是两类调用方的诉求本来就不一样—— 外部要的是"好懂、好调试、稳定";内部要的是"快、强类型、可演进"。

GraphQL 的诚实评价:它解决的是"多个客户端要不同字段"的问题。 如果你只有一个 Web 端,它带来的复杂度(缓存、N+1、权限、监控)通常大于收益。


二、REST 设计:把约定写死 ​

2.1 URL:名词复数 + 层级表达关系 ​

GET    /users              # 列表
POST   /users              # 创建
GET    /users/{id}         # 详情
PATCH  /users/{id}         # 部分更新
DELETE /users/{id}         # 删除

GET    /users/{id}/orders  # 子资源
POST   /orders/{id}/cancel # 非 CRUD 动作:动词放末尾
规则说明
用名词复数/users 而非 /getUser
层级不超过两层更深就用查询参数过滤
非 CRUD 动作用动词结尾/orders/{id}/cancel
不用动词做资源名❌ /createUser

2.2 状态码:只用常见的那几个 ​

码含义典型场景
200成功查询、更新成功
201已创建POST 成功,响应应带 Location
204无内容删除成功
400请求错误参数校验失败(客户端的错)
401未认证没带 token 或 token 失效
403无权限认证了但不够格
404不存在也可能是故意不告诉你存在(权限场景)
409冲突资源状态冲突、唯一键重复
422语义错误格式对但业务规则不满足
429限流必须带 Retry-After
500服务端错误兜底

⚠️ 别用 200 包一切。把错误塞进 200 + {code: 1} 会让: HTTP 层的重试、缓存、监控、网关限流全部失效。

2.3 错误体:要能被程序处理 ​

json
{
  "error": {
    "code": "ORDER_ALREADY_PAID",
    "message": "该订单已支付,无法取消",
    "details": [
      { "field": "orderId", "reason": "状态不允许" }
    ],
    "requestId": "req_abc123"
  }
}
字段为什么要有
code稳定的机器可读标识,客户端按它分支,不要按 message
message给人看的,不要用于逻辑判断
details表单校验错误的字段级信息
requestId排障的关键,前后端日志靠它对齐

2.4 分页:游标优先于页码 ​

方式问题
?page=1&size=20数据变动时会重复或漏掉记录;深翻页慢
?cursor=xxx&size=20需要排序键,但稳定

列表会增长、会按时间排序 → 用游标。 后台管理这种数据量小且稳定的场景,页码也可以接受。


三、版本兼容:最重要的一节 ​

API 一旦发布,就有了客户端。改它是成本最高的操作。

3.1 先分清"兼容"与"不兼容" ​

改动兼容性
新增可选字段✅ 兼容
新增接口✅ 兼容
新增错误码⚠️ 客户端可能不认识
删除字段❌ 破坏性
改字段类型/含义❌ 破坏性
把可选改必填❌ 破坏性
改枚举值语义❌ 破坏性

💡 一条硬规则:响应里的字段只增不删。 废弃字段标 @deprecated 并继续返回,等确认无调用方后再下线。

3.2 三种版本策略 ​

策略做法适用
URL 版本/v1/users对外开放 API,最常见也最清晰
Header 版本Accept: application/vnd.api.v2+json不想污染 URL;但调试不便
不版本,靠演进只做兼容改动内部服务、客户端可控

判断标准:

  • 客户端你控制不了(第三方、App 老版本)→ 必须版本化
  • 前后端一起发版、可强制升级 → 可以只做兼容演进

3.3 破坏性变更的落地流程 ​

1. 新老并存:/v1 保持,/v2 上线
2. 双写过渡期:监控 /v1 的调用量
3. 通知调用方,给明确的迁移期限
4. /v1 调用量归零后再下线

⚠️ 千万不要"发个公告就删"。App 老版本的存活周期远超你的预期。


四、幂等:写接口必须设计 ​

网络会重试、用户会连点、消息会重投。写操作必须能重复执行而不出错。

手段适用
幂等键(客户端生成 Idempotency-Key)创建类接口,防重复下单
唯一索引数据库层兜底
状态机校验已支付的订单不允许再支付
天然幂等(PUT 覆盖、DELETE)直接用

判断标准:如果这个接口被调用两次会产生两笔数据,它就需要幂等设计。


五、自检清单 ​

  • [ ] URL 用名词复数,非 CRUD 动作用动词结尾
  • [ ] 状态码语义正确,不用 200 包错误
  • [ ] 错误体有 code(机器读)、requestId(排障)
  • [ ] 列表分页用游标(会增长的列表)
  • [ ] 响应字段只增不删
  • [ ] 有版本策略,且知道什么情况下必须版本化
  • [ ] 所有写接口都考虑过幂等
  • [ ] 限流返回 429 且带 Retry-After
  • [ ] 对外 API 有文档(OpenAPI 自动生成也算)

六、常见误区 ​

误区事实
"REST 就是 CRUD"REST 是资源建模,非 CRUD 动作要用动词子资源表达
"gRPC 更先进,该全面替换 REST"浏览器直连麻烦、调试不便。对外场景 REST 更合适
"GraphQL 能解决过度获取,就该上"它解决的是多端差异;单端场景带来的复杂度常常不划算
"接口先上线,版本以后再说"一旦有客户端,加版本就是破坏性变更
"错误码用数字就行"用语义化字符串,数字码会失控且不自解释
"字段不用了就删掉"删除即破坏性变更。标废弃,保留返回
"重试是客户端的事"服务端不做幂等,重试必然产生脏数据

七、动手练习 ​

练习 A:找出破坏性变更 ​

给出一组接口改动,判断哪些是破坏性的、哪些兼容: 新增可选字段 / 删除响应字段 / 把 amount 从字符串改数字 / 新增必填请求参数 / 新增错误码。

练习 B:设计一个会重试的下单接口 ​

要求:用户连点两次只生成一笔订单。 写出你需要的东西(幂等键放哪、存在哪、重复请求返回什么)。


相关文档 ​

本区 ​

其他域 ​