为什么 REST API 需要设计规范

很多团队的后端接口,是随着业务一路“长”出来的:有人写 /getUserInfo,有人写 /user/get,还有人干脆用 /api/doSomething;成功时返回 {"success":true},失败时又换成 {"code":500,"msg":"..."};更有甚者,把所有响应的 HTTP 状态码都设成 200,把真正的错误塞进响应体里。接口一多,前端要维护十几种解析逻辑,文档永远落后于实现,联调成本高得离谱。

REST(Representational State Transfer,表述性状态转移)不是某家公司颁布的标准,而是一种架构风格。它由 Roy Fielding 在博士论文中提出,核心思想是把业务数据抽象成“资源”,再用一套统一的接口去操作这些资源。落地时我们要做的,是遵循它的一组约束,而不是背诵它的定义。

REST 的六条约束中,对日常接口设计影响最大的是这几条:

  • 客户端-服务器分离:前后端各自独立演进,接口就是双方共同遵守的契约。
  • 无状态:每个请求必须自带完整上下文,服务端不保存会话状态,因此可以随意水平扩展。
  • 可缓存:GET 响应应当能够被缓存,用 Cache-Control、ETag 明确表达缓存策略。
  • 统一接口:用同一套 HTTP 方法与同一套 URL 约定操作所有资源,降低认知负担。
本文核心知识点
  • 资源导向URL 用名词复数表示资源,动作用 HTTP 方法表达,避免在路径里塞动词。
  • 状态码语义用 201、204、400、404、409、422 等表达结果,不要一律返回 200。
  • 统一契约请求与响应的字段命名、时间格式、错误结构全站保持一致。
  • 幂等设计用 Idempotency-Key 让重试安全,让弱网与超时不再产生重复订单。
REST API 设计示意:蓝色渐变背景上的发光接口模块、数据管道与层叠的接口契约卡片
统一接口与契约:一个网关模块向四周延伸出数据管道,接口文档卡片承载着稳定的字段约定,这是 REST 风格最直观的样子。

资源与 URL 设计:以名词为中心

REST 的第一条直觉是:URL 用来定位资源,方法用来表达动作。资源要用名词,并且统一用复数;动作交给 HTTP 方法,不要出现在路径里。集合与单个成员用同一套命名,靠路径层级区分。

场景推荐写法不推荐写法
查询用户列表GET /v1/usersGET /v1/getUserList
查询单个用户GET /v1/users/42GET /v1/user?id=42
创建用户POST /v1/usersGET /v1/users/create
更新用户PATCH /v1/users/42POST /v1/users/update
删除用户DELETE /v1/users/42POST /v1/users/delete

层级关系用路径表达,但嵌套最多两层。例如“用户的订单”可以写成 /v1/users/42/orders,再往下就应换成由订单侧主导的资源,而不是继续堆叠。过滤、搜索、排序这类不属于资源标识的条件,统一放进查询字符串:/v1/orders?status=paid&sort=-createdAt。路径中出现大写字母、下划线或动词,几乎都是设计走偏的信号。

HTTP 方法与状态码的正确用法

HTTP 方法自带语义,其中“安全”指不改变服务端状态,“幂等”指重复执行与执行一次的效果相同。理解这两个属性,能帮你判断一个接口能不能安全重试、能不能被缓存。

方法语义安全幂等
GET读取资源,不产生副作用是是
POST创建资源或触发处理否否
PUT整体替换资源否是
PATCH局部更新资源否否
DELETE删除资源否是

状态码是接口最重要的“语义出口”。一个常见误区是“只要服务没崩就返回 200”,结果把业务失败、权限不足、参数错误都藏进响应体。正确做法是用状态码表达结果层级,让网关、监控和客户端都能做统一处理:

状态码含义典型场景
201 Created资源创建成功POST 创建后返回,并带上 Location 头
204 No Content成功但无响应体DELETE 成功、PUT 无返回内容
400 Bad Request请求格式错误JSON 解析失败、必需字段缺失
401 / 403未认证 / 无权限Token 缺失或已过期、角色不允许
404 Not Found资源不存在id 无效,或无权知晓其存在
409 Conflict状态冲突重复创建、乐观锁版本不一致
422 Unprocessable语义校验失败格式合法但字段取值不满足业务规则

请求与响应格式:统一 JSON 契约

格式统一比格式“优雅”更重要。字段命名要么全用 camelCase,要么全用 snake_case,不要在同一个项目里混用;时间统一用 ISO 8601 的 UTC 字符串;金额用整数分或字符串,避免浮点误差;布尔值不要用 0/1 或 "true"/"false" 这类字符串。下面是一个创建订单的请求与响应示例:

POST /v1/orders
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "skuId": "SKU-10086",
  "quantity": 2,
  "remark": "工作日白天送达"
}

HTTP/1.1 201 Created
Location: /v1/orders/90210

{
  "id": "90210",
  "skuId": "SKU-10086",
  "quantity": 2,
  "amountCent": 13900,
  "currency": "CNY",
  "status": "created",
  "createdAt": "2026-10-14T03:20:11Z"
}

字段约定上,还有几条经验值得固化进团队规范:

  • 资源用扁平结构,不要把无关字段层层套娃;确实需要时用固定命名空间。
  • 列表响应外层包一层 data 与分页字段,而不是让数组裸奔,便于日后扩展。
  • 可空字段给出 null 而不是省略键,避免前端判断 undefined 与 null 两套逻辑。
  • 枚举值用小写英文,并在文档里列出全部取值,客户端不要写死未知值。

版本管理:URL、Header 还是内容协商

接口一旦对外发布,就必然面临演进。破坏性变更(删字段、改类型、改语义)必须通过版本隔离,非破坏性变更(新增可选字段)则可以直接上线。常见的版本方案有三种:

  • URL 版本:/v1/users。最直观,路由、日志、缓存都容易区分,缺点是版本号“污染”了资源标识。
  • 自定义请求头:X-Api-Version: 2026-10-14。URL 干净,但调试时不直观,缓存和网关需要额外配置。
  • 内容协商:Accept: application/vnd.api.v2+json。最符合 REST 精神,但实现与排障成本最高。

对绝大多数团队,URL 主版本号是最实用的选择:简单、可见、易于回滚。真正重要的是把“什么算破坏性变更”写成白纸黑字,并在 CI 里加一道响应结构快照对比,防止有人在 /v1 上悄悄删掉一个字段。

分页、过滤与排序的约定

列表接口是复用率最高的接口,也是最容易设计失控的接口。分页参数、过滤参数、排序参数的命名一旦统一,前端就能写出通用的列表组件。

参数说明示例
page / pageSize页码从 1 开始,pageSize 设上限?page=1&pageSize=20
cursor游标分页,适合大数据量与实时流?cursor=eyJpZCI6OTAyfQ
filter按字段精确过滤?status=paid&channel=app
sort字段名前加 - 表示倒序?sort=-createdAt,name
q关键词模糊搜索?q=键盘

偏移分页(offset/limit)实现简单,但页码越深、数据库扫描越多,且在数据频繁写入时会出现“重复或漏项”。订单、日志、消息这类高频写入的列表,建议直接采用游标分页:把上一页最后一条的排序键编码成不透明游标,下一页从该位置继续取。响应里同时给出总数(可选)、是否还有下一页以及下一页游标,客户端无需关心服务端如何实现。