文章

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

牛耕田

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

一、什么是 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
  • 不要用动词getcreatedelete),语义交给 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 状态码的正确使用

状态码 含义 使用场景
200 OK 查询/更新成功
201 Created 创建成功,Location 头返回新资源地址
204 No Content 删除成功,无返回体
400 Bad Request 参数校验失败
401 Unauthorized 未登录(缺凭证)
403 Forbidden 已登录但无权限
404 Not Found 资源不存在
409 Conflict 冲突(如用户名已存在)
429 Too Many Requests 限流
500 Internal Server Error 服务端异常
502 / 504 Bad 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 3springdoc-openapi 自动生成文档,注解即文档。
  • Knife4j:基于 Swagger 的增强 UI,国内项目常用。
  • 生产环境关闭 Swagger UIspringdoc.api-docs.enabled=false),避免接口结构泄露。

十、小结

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