面试造火箭(HATEOAS),入职拧螺丝(
GET /users+POST /users)。这是大多数后端开发对 REST 的真实体验。
引言
REST 这个词被滥用得太厉害了。很多人觉得"URL 里用名词、返回 JSON"就是 RESTful------但这只能算 Level 2。真正的 REST(Fielding 博士论文里的那个)要求 HATEOAS,而它几乎从未在工业界大规模落地过。
这篇文章从 Richardson 成熟度模型出发,把 REST 的四个等级讲清楚,然后回到实际:你的项目到底需要做到哪一级?哪些场景 REST 根本不适用?
Richardson 成熟度模型:REST 的四级台阶
Leonard Richardson 在 2008 年提出了一个衡量 API RESTful 程度的模型,Martin Fowler 后来在博客中推广了它。
Level 0:单一 URI + 单一方法
http
POST /api/service
Content-Type: application/json
{
"action": "getUser",
"params": { "id": 1 }
}
所有操作通过一个 URI 和一个 HTTP 方法(通常是 POST)完成。请求体里携带操作名和参数。这是 SOAP 时代的遗产------HTTP 被当成传输层,而不是应用层协议。
Level 1:资源 URI
http
GET /users/1
GET /users/1/orders
不同的资源有了不同的 URI,但还没有充分利用 HTTP 方法------可能增删改查全是 GET 或 POST。进步是有了资源概念,缺点是操作语义仍靠自定义参数传递。
Level 2:HTTP 动词(绝大多数项目的终点)
http
GET /users/1 # 查询
POST /users # 创建
PUT /users/1 # 全量更新
PATCH /users/1 # 部分更新
DELETE /users/1 # 删除
每个 HTTP 动词有明确的语义:GET 是安全且幂等的,PUT 是幂等的,POST 非幂等,DELETE 是幂等的。同时合理使用 HTTP 状态码:
| 状态码 | 语义 | 使用场景 |
|---|---|---|
| 200 | OK | GET/PUT/PATCH 成功 |
| 201 | Created | POST 创建资源成功 |
| 204 | No Content | DELETE 成功 |
| 400 | Bad Request | 参数校验失败 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 资源冲突(如重复创建) |
| 422 | Unprocessable | 语义错误 |
| 500 | Internal Error | 服务端异常 |
Level 2 是现代 Web API 的实际标准。Swagger/OpenAPI 对这个级别的支持最好,工具链也最成熟。
Level 3:HATEOAS(超媒体即应用状态引擎)
http
GET /users/1
200 OK
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com",
"_links": {
"self": { "href": "/users/1" },
"orders": { "href": "/users/1/orders" },
"deactivate": { "href": "/users/1/deactivate" },
"department": { "href": "/departments/5" }
}
}
服务端在响应中告诉客户端"基于当前资源状态,你可以做哪些操作"。理论优势是客户端和服务端解耦------服务端改 URL 结构,客户端无需更新,因为它总是从响应中动态发现链接。
Richardson 模型认为只有到达 Level 3 才算真正的 REST。但这个要求在工业界几乎从未被满足。
为什么 Level 3 在实践中失败了?
1. 前端复杂度急剧增加
传统调用:
javascript
// 简单直接
const user = await fetch('/users/1');
const orders = await fetch(`/users/1/orders`);
HATEOAS 调用:
javascript
// 需要解析、查找、匹配
const userResp = await fetch('/users/1');
const userData = await userResp.json();
const ordersLink = userData._links.find(l => l.rel === 'orders');
if (!ordersLink) throw new Error('No orders link available');
const orders = await fetch(ordersLink.href);
代码量翻倍,而且这些链接结构几乎不会变------你为"灵活性"付出了"复杂性",而灵活性从未被使用。
2. 移动端不友好
App 发布有审核周期,不能像 Web 页面那样热更新。HATEOAS 的核心价值"服务端随意改 URL"对移动端完全无效------App 里的 URL 都硬编码在代码里。
3. OpenAPI/Swagger 支持差
OpenAPI 3.0 引入了 links 字段支持有限的 HATEOAS 描述,但没有一个主流工具能自动从 HATEOAS 响应生成可用的文档。这意味着你实现了 HATEOAS,但反而失去了 API 文档的可读性。
4. 没有成功的商业案例
PayPal、GitHub、Stripe、Twilio------所有顶级 API 提供商都停留在 Level 2。没有一个公开的、大规模使用的 Level 3 API。这不是巧合。
你应该关心的:REST 设计中的务实选择
1. URL 设计:名词复数 + 层级不超过 2 层
bash
✅ GET /users # 资源列表
✅ GET /users/1 # 单个资源
✅ GET /users/1/orders # 子资源
✅ POST /users # 创建
✅ PUT /users/1 # 全量更新
✅ PATCH /users/1 # 部分更新
✅ DELETE /users/1 # 删除
❌ GET /getUser?id=1 # 动词 + 查询参数
❌ POST /createUser # 动词
❌ GET /users/1/orders/2/items/3/comments # 4 层嵌套
当嵌套超过 2 层,有两种解:
- 独立资源:
GET /comments?user_id=1 - BFF 聚合:一个
/api/page/detail聚合多个后端服务
2. 版本管理:URL 路径 vs Header
http
# 方式 A:URL 路径(直观,调试方便)
GET /api/v1/users
# 方式 B:自定义 Header(不污染 URL)
GET /api/users
Accept: application/vnd.myapp.v1+json
两种都可以,关键是全团队统一。方式 A 在 API 网关做路由更方便,方式 B 更符合 HTTP 标准。如果有 API 网关,选 A;如果对外提供 SDK,选 B。
3. 分页、过滤、排序的标准化
http
GET /users?page=2&per_page=20&sort=-created_at&role=admin
返回格式统一:
json
{
"data": [...],
"meta": {
"current_page": 2,
"per_page": 20,
"total": 156,
"total_pages": 8
},
"links": {
"first": "/users?page=1&per_page=20",
"prev": "/users?page=1&per_page=20",
"next": "/users?page=3&per_page=20",
"last": "/users?page=8&per_page=20"
}
}
注意这里的 links 只是分页辅助信息,不是 HATEOAS------你不需要从 links 中动态发现如何翻页,这只是给客户端提供便利的预计算 URL。
4. 错误响应格式统一
json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email format is invalid",
"details": [
{ "field": "email", "reason": "invalid_format" }
]
}
}
比返回 { "code": 500, "msg": "error" } 强一万倍------后者叫"把 HTTP 降级为传输层"。
REST 不是万能药:什么时候不该用 REST?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 微服务内部调用 | gRPC | 强类型、高性能、代码生成 |
| 移动端首页聚合 | GraphQL 或 BFF | 一次请求获取多种数据 |
| 实时通信 | WebSocket / SSE | 双向推送,REST 做不到 |
| 文件上传 | REST + 分片 | POST /upload + multipart |
| 操作类接口 | RPC 风格 | POST /orders/123/cancel 比 PATCH /orders/123 语义更清晰 |
操作类接口用 RPC 风格不是罪过。 POST /orders/123/cancel 比"硬套 REST 改成 PATCH /orders/123 加自定义字段"要清晰得多。语义清晰 > 范式纯正。
总结:你到底需要做到哪一级?
| 项目类型 | 最低要求 | 推荐 |
|---|---|---|
| 个人项目 / MVP | Level 1 | Level 2(简单) |
| 内部管理系统 | Level 2 | Level 2 |
| 对外开放 API | Level 2 + 文档 | Level 2 + OpenAPI |
| 微服务间调用 | --- | gRPC |
| 理论论文 | Level 3 | --- |
Level 2 就是工业界的最佳实践。 如果有人跟你说"不用 HATEOAS 就不是 REST",你的回答是:"对,我做的就是 Level 2 的 HTTP API,不是 Roy Fielding 论文里的 REST------这是两个概念。"
参考链接:
如果这篇文章帮你理清了 REST 的务实设计思路,欢迎点赞、收藏。有不同观点评论区聊。