RESTful API 设计规范与版本演进
接口设计这件事,写的时候没人觉得难,维护的时候才知道痛。一个订单列表接口,第一版叫 getOrderList,第二版加了分页参数 page,第三版发现游标分页更好又加了 cursor,第四版有人搞了个 getOrderListV2 和前者并存。半年后没人说得清该调哪个。
这篇聊的就是怎么把接口设计得经得起时间折腾。以 REST 风格为主线,但原则本身和框架无关。
资源命名,先把词表定下来
REST 的核心是把接口看成资源而不是动作。URL 用名词、HTTP 动词表达意图:
bash
GET /orders 查订单列表
GET /orders/123 查单个订单
POST /orders 创建订单
PUT /orders/123 全量更新
PATCH /orders/123 部分更新
DELETE /orders/123 删除订单
几条容易跑偏的地方:
- 名词用复数。
/orders而不是/order,全 API 统一,别一半单数一半复数 - 层级表达从属关系:
/users/45/orders表达"用户 45 的订单",但层级别超过两层,再深就用查询参数:/orders?user_id=45 - URL 里不要出现动词。
/orders/create、/getOrderList这类 RPC 风格命名和资源风格混着用,是接口腐化的开始 - 非 CRUD 的业务动作不好用名词表达时,可以退一步用子资源:
POST /orders/123/cancellation发起取消
字段命名也一并定死:JSON 用小驼峰还是下划线无所谓,关键是全 API 一致。字段名是 API 的词表,词表乱了,前端就得在每个接口里猜一遍。
状态码是协议的一部分,不是装饰
200 包一切、错误塞在 body 里的 {"code": 500, "msg": "..."} 写法很普遍,但它把 HTTP 协议自带的语义丢了。网关、监控、重试逻辑全都依赖状态码做判断,全 200 会让这些基础设施失明。
按语义用:
200成功,201创建成功(配 Location 头指向新资源),204删除成功无返回体400参数错误(body 里给出哪个字段错了),401未认证,403无权限,404资源不存在409冲突(重复创建、并发修改),422业务校验失败,429限流500服务端异常,502/503/504网关和依赖服务问题
错误响应的结构要统一,且带足够的排错信息:
css
{
"error": {
"code": "ORDER_ALREADY_SHIPPED",
"message": "订单已发货,无法修改收货地址",
"trace_id": "req-a1b2c3d4"
}
}
错误码用字符串常量而不是数字,ORDER_ALREADY_SHIPPED 比 40012 可读得多,调用方判断也不用维护一张数字对照表。
分页、过滤、排序的统一约定
列表接口早晚要面对这三个需求,一开始就定好格式,省得每个接口各造一套:
ini
GET /orders?page=1&page_size=20
GET /orders?cursor=eyJvIjoxMjM0fQ&limit=20
GET /orders?status=pending&created_after=2026-01-01
GET /orders?sort=-created_at,amount
两种分页各有位置。页码分页直观、支持跳页,适合后台管理界面;游标分页性能稳定(深翻页不退化)、数据实时性好,适合信息流和移动端无限滚动。判断标准看产品形态,别只用一种。
响应里除了数据列表,把分页元信息也带上:
yaml
{
"data": [...],
"paging": {
"total": 1024,
"page": 1,
"page_size": 20
}
}
时间字段统一用 ISO 8601 带时区(2026-03-15T08:30:00+08:00),别用时间戳数字裸奔------调用方每次都要写一遍转换,时区出错还特别难查。
幂等,支付和创建类接口的保命设计
网络超时后调用方重试,一个创建订单的请求被执行两次,用户被扣了两次钱。这类事故的解法是幂等键:
bash
POST /orders
Idempotency-Key: 6f1c8a2e-xxxx-xxxx
服务端记录这个键,同一个键的重复请求返回第一次的结果(或者直接告诉调用方"已处理过"),不再执行业务。创建、支付、退款这类有资金或副作用语义的接口,幂等设计是必选项,不是可选项。
PUT 和 DELETE 天然幂等(同样的请求执行多次结果不变),POST 不是,所以幂等键主要给 POST 用。
版本演进:不破坏比什么都重要
API 一旦有人用,每个改动都是公共事件。原则很简单:可以加,不能改,更不能删。
加字段、加可选参数、加新的枚举值,这些是安全的,调用方无感知。改字段名、改字段类型、改语义(原来传 0 表示不限,改成 0 表示精确匹配零),这些是破坏性变更。
真要做破坏性变更,走版本升级路线:
bash
GET /v1/orders
GET /v2/orders
URL 版本化最直观,网关路由也容易做。老版本给一个明确的下线时间表,期间双版本并存、监控老版本流量归零后再下线。直接下线老版本的动作,相当于替你的调用方决定他们的排期,合作基本就到头了。
没有版本号的情况下也有补救手段:新能力用可选参数渐进灰度(feature flag 控制默认值),行为差异写进变更日志并在响应头里带版本提示(X-API-Deprecated: true),给调用方足够的迁移信号。
把规范固化成工程约束
口头规范活不过三个人。真正起作用的规范是写进工具里的:
- OpenAPI(Swagger)描述文件作为接口的单一事实来源,评审先看文档再看代码
- lint 工具扫 URL 命名、字段命名的一致性,不一致的 CI 直接挡下
- mock server 从 OpenAPI 自动生成,前后端并行开发不互相等
- 契约测试保证实现和文档不漂移
规范的价值不在文档写得漂亮,而在三个月后新同学照着做出来的接口,和团队里老人写的看不出区别。
领域:后端设计
关键词:RESTful、API设计、幂等性、版本管理、OpenAPI