文章

RESTful API 设计规范:从命名到版本控制

更新于 2026/09/20牛耕田

暂存笔记,持续补充中。接口设计是后端工程师的「门面」,规范与否直接影响协作效率。

一、什么是 REST#

REST(Representational State Transfer)是一种架构风格,不是协议。核心约束:

约束含义
客户端-服务器分离前后端各自演进
无状态每个请求自带全部上下文,服务端不存会话
可缓存响应显式声明可缓存性
统一接口用统一的方式操作资源
分层系统允许中间层(网关、CDN)

REST 的心智模型:一切皆资源,用 HTTP 动词表达对资源的操作,用 URL 定位资源。

二、URL 设计规范#

✅ 推荐
GET /api/v1/users 查询用户列表
GET /api/v1/users/123 查询单个用户
POST /api/v1/users 创建用户
PUT /api/v1/users/123 全量更新
PATCH /api/v1/users/123 部分更新
DELETE /api/v1/users/123 删除用户
GET /api/v1/users/123/orders 查该用户的订单(子资源)
❌ 反面教材
GET /api/getUserById?id=123 动词出现在 URL
POST /api/deleteUser 用 POST 做删除
GET /api/user/list 名词用复数不一致

命名规则:

  • 用名词复数表示资源集合:/users 而非 /user。
  • 用小写 + 连字符:/order-items 而非 /orderItems 或 /order_items。
  • 不要用动词(get、create、delete),语义交给 HTTP 方法。
  • 层级不超过两层,再深用查询参数:/orders?userId=123 优于 /users/1/orders/2/items/3。
  • 查询过滤用 query string:/users?status=active&page=1&size=20。

三、HTTP 方法与幂等性#

方法语义幂等安全
GET查询✅✅
POST创建❌❌
PUT全量替换✅❌
PATCH部分更新❌(一般不保证)❌
DELETE删除✅❌

幂等:执行一次和执行 N 次效果相同。这对网络重试至关重要——支付、下单接口必须做幂等。

PUT vs PATCH:

  • PUT 是全量替换,未传的字段会被置空。
  • PATCH 是局部更新,只改传入的字段(生产环境更常用)。

四、统一响应体#

{
"code": 0,
"message": "success",
"data": { "id": 123, "name": "张三" },
"timestamp": 1758384000000
}

分页响应的标准结构:

{
"code": 0,
"message": "success",
"data": {
"list": [ { "id": 1 }, { "id": 2 } ],
"total": 137,
"page": 1,
"size": 20,
"pages": 7
}
}

Java 实现:

@Data
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<>();
r.code = 0;
r.message = "success";
r.data = data;
return r;
}
public static <T> Result<T> fail(int code, String msg) {
Result<T> r = new Result<>();
r.code = code;
r.message = msg;
return r;
}
}

为什么要包一层? 便于前端统一拦截处理(code != 0 时直接弹错误提示),并预留了扩展字段(traceId、分页信息)。

⚠️ 业务 code 与 HTTP 状态码要配合使用:HTTP 状态码表达传输层结果,业务 code 表达业务层结果。

五、HTTP 状态码的正确使用#

状态码含义使用场景
200OK查询/更新成功
201Created创建成功,Location 头返回新资源地址
204No Content删除成功,无返回体
400Bad Request参数校验失败
401Unauthorized未登录(缺凭证)
403Forbidden已登录但无权限
404Not Found资源不存在
409Conflict冲突(如用户名已存在)
429Too Many Requests限流
500Internal Server Error服务端异常
502 / 504Bad Gateway / Gateway Timeout网关/上游超时

401 vs 403 常见误区:401 是「你是谁我不知道」,403 是「我知道你是谁,但你没权限」。

六、幂等性设计#

为什么需要:用户重复点击提交、网络超时重试、消息队列重复消费,都会导致重复请求。

方案:

  1. 唯一索引(最可靠):订单表对 order_no 建唯一索引,重复插入直接抛异常。
  2. Token 机制:下单前先调 /token 拿一次性令牌,提交时带上,服务端用 Redis 原子删除校验。
public void submit(String token, OrderDTO dto) {
// SETNX 保证原子性
Boolean ok = redis.opsForValue()
.setIfAbsent("idem:" + token, "1", Duration.ofMinutes(10));
if (Boolean.FALSE.equals(ok)) {
throw new BizException("请勿重复提交");
}
orderService.create(dto);
}
  1. 状态机:只允许「待支付 → 已支付」,重复回调时状态已变更则直接返回成功。
// 乐观锁更新,靠影响行数判断
int rows = orderMapper.updateStatus(orderId,
OrderStatus.PAID, OrderStatus.UNPAID); // WHERE status = UNPAID
if (rows == 0) {
log.info("订单已处理,忽略重复回调 orderId={}", orderId);
return;
}

七、API 版本控制#

方案示例评价
URL 路径(主流)/api/v1/users直观、易调试、便于网关路由
请求头Accept: application/vnd.api.v1+json语义纯粹,但调试不便
查询参数/api/users?version=1简单,但污染参数

实践建议:

  • 用 URL 路径版本控制,网关按前缀分流。
  • 只对不兼容变更升版本,兼容性新增字段不用升。
  • 旧版本要设弃用时间表,并在响应头加 Deprecation / Sunset 提示。

什么算不兼容变更:删除字段、改字段类型、改字段含义、改必填性、改错误码含义。 什么算兼容变更:新增可选字段、新增接口、新增枚举值(前端需容错)。

八、安全与限流#

  • 认证:Authorization: Bearer <token>(详见 Spring Security 笔记)。
  • 限流:Redis + Lua 滑动窗口,或网关层(Sentinel / Nginx)限流。
  • 敏感字段不出参:DTO 与 Entity 分离,密码、手机号中间四位必须脱敏。
  • 防参数污染:/users?role=admin&role=user,服务端必须明确取第一个还是拒绝。
  • CORS:明确 Access-Control-Allow-Origin,生产环境禁止用 * 配合 Allow-Credentials: true。

九、文档与调试#

  • Swagger / OpenAPI 3:springdoc-openapi 自动生成文档,注解即文档。
  • Knife4j:基于 Swagger 的增强 UI,国内项目常用。
  • 生产环境关闭 Swagger UI(springdoc.api-docs.enabled=false),避免接口结构泄露。

十、小结#

  • REST 的本质:资源用名词、操作靠 HTTP 方法、状态用状态码。
  • URL 用复数名词、小写连字符、不超过两层。
  • 统一响应体 + 合理状态码 + 分页规范是团队协作的底线。
  • 写接口前先问:这个接口幂等吗? 涉及钱的接口必须幂等。
  • 版本控制用 URL 路径,只对不兼容变更升版本。
返回主页