# REST 的四个成熟度等级:为什么你不需要 Level 3

面试造火箭(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/cancelPATCH /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 的务实设计思路,欢迎点赞、收藏。有不同观点评论区聊。

相关推荐
万少1 小时前
22 点后,我靠这个 AI 工具成了"夜间天才程序员"
前端·后端
IT_陈寒2 小时前
React hooks 闭包陷阱把我的状态吃掉了,原来问题出在这里
前端·人工智能·后端
壹方秘境2 小时前
使用ApiCatcher在 iOS 上像修改 hosts 一样自定义域名解析
前端·后端·客户端
葫芦和十三3 小时前
图解 MongoDB 22|读写关注:持久性与一致性的档位选择
后端·mongodb·agent
葫芦和十三9 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp10 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑10 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯11 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan13 小时前
多Agent之间的区别
后端