文章
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。 - 不要用动词(
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 状态码的正确使用
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 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 是「我知道你是谁,但你没权限」。
六、幂等性设计
为什么需要:用户重复点击提交、网络超时重试、消息队列重复消费,都会导致重复请求。
方案:
- 唯一索引(最可靠):订单表对
order_no建唯一索引,重复插入直接抛异常。 - 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);
}
- 状态机:只允许「待支付 → 已支付」,重复回调时状态已变更则直接返回成功。
// 乐观锁更新,靠影响行数判断
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 路径,只对不兼容变更升版本。