3. RESTful API 设计原则:资源命名规范、无状态约束、统一接口、HATEOAS 概念入门

说到 RESTful API,很多人第一反应就是「用 GET/POST/PUT/DELETE 写 CRUD」。嗯,这没错,但只对了一半。REST 的核心其实是一套设计哲学,而不是单纯的 HTTP 动词映射。我在早期接手过一个内部系统,接口命名五花八门——/getUserInfo、/createNewOrder、/deleteItemById……维护起来简直是一场噩梦。后来我们花了两个迭代全部重构为 RESTful 风格,团队沟通效率明显提升。

这一节,我会把 RESTful 的四个核心原则拆开来讲。你不需要死记硬背,理解背后的设计意图就好。

3.1 资源命名规范:用名词,别用动词

REST 把一切视为「资源」。用户、订单、商品、评论……都是资源。资源的命名应该用名词复数,而不是动词短语。

核心规则:

  • 使用名词复数:/users、/orders、/products
  • 避免动词:不要用 /getUsers、/createOrder
  • 层级关系用斜杠:/users/{userId}/orders
  • 查询参数用于过滤/排序:/orders?status=paid&page=1

举个例子。假设你要获取某个用户的所有订单:

# 不好的做法
GET /getUserOrders?userId=123

# 好的做法
GET /users/123/orders

为什么?因为前者把操作(get)写进了路径,后者把用户和订单的从属关系表达得清清楚楚。我在项目中见过有人把 /deleteUser 和 /removeUser 混着用,前端同事每次都要翻文档确认——这种混乱完全可以避免。

小技巧: 如果某个操作确实不像资源(比如「登录」),可以用动词,但尽量少用。登录可以设计为 POST /sessions,注销为 DELETE /sessions/current。

3.2 无状态约束:每次请求都是独立的

无状态(Stateless)是 REST 里最容易忽略、也最容易出问题的一条。说白了,服务器不保存客户端的状态。每次请求都必须包含所有必要的信息,服务器不依赖之前的请求来理解当前请求。

为什么会这样?因为无状态让系统更容易水平扩展。你想想看,如果服务器 A 保存了用户的登录状态,下次请求被负载均衡转发到服务器 B,B 不知道这个用户是谁——那就得搞 session 共享,麻烦得很。

我曾经在一个电商项目中踩过这个坑。早期用 session 保存购物车数据,结果用户反馈「加进购物车的商品刷新后不见了」。查了半天,原来是 session 过期时间设置太短。后来我们把购物车数据放到客户端(localStorage),每次请求带上 token 和购物车 ID,问题就解决了。

注意: 无状态不代表「不能有状态」。认证信息(如 JWT token)可以放在请求头里,由客户端维护。服务器只负责验证,不负责存储。

无状态的好处很明显:

  • 任意服务器都可以处理任意请求
  • 故障转移简单,重启服务器不会丢数据
  • 缓存策略更容易实现

3.3 统一接口:用标准方法操作资源

统一接口的意思是:所有资源都通过同一套标准方法进行操作。HTTP 已经给我们提供了这些方法:

HTTP 方法 操作 示例 说明
GET 读取资源 GET /users/123 幂等,不改变服务器状态
POST 创建资源 POST /users 非幂等,每次创建新资源
PUT 全量更新 PUT /users/123 幂等,替换整个资源
PATCH 部分更新 PATCH /users/123 非幂等(视实现而定)
DELETE 删除资源 DELETE /users/123 幂等,删除后返回 204

我见过不少团队把 POST 当万能方法用——查询用 POST、删除也用 POST。这样做虽然能跑,但违背了统一接口的原则。客户端无法通过方法名推断操作意图,缓存、代理、防火墙等中间件也无法正常工作。

一个简单的判断标准: 如果操作是幂等的(多次执行结果相同),用 PUT 或 DELETE;如果每次执行都会产生新资源或副作用,用 POST。

3.4 HATEOAS 概念入门:让 API 自描述

HATEOAS(Hypermedia As The Engine Of Application State)是 REST 里最「重」的一个概念,也是实际项目中最容易被忽略的。它的核心思想是:API 返回的响应里,应该包含下一步可以做什么的链接。

举个例子。你请求一个订单:

GET /orders/123

{
  "id": 123,
  "status": "paid",
  "total": 299.00,
  "links": [
    { "rel": "self", "href": "/orders/123" },
    { "rel": "cancel", "href": "/orders/123/cancel", "method": "POST" },
    { "rel": "refund", "href": "/orders/123/refund", "method": "POST" },
    { "rel": "items", "href": "/orders/123/items" }
  ]
}

客户端看到 links 就知道:这个订单可以取消、可以退款、可以查看商品明细。不需要事先查文档。这就是 HATEOAS 的魅力——API 自己告诉你怎么用。

说实话,我在实际项目中很少见到完全遵循 HATEOAS 的 API。大部分团队觉得「太麻烦」「前端自己拼 URL 就行了」。但如果你在做一个开放平台,或者 API 的生命周期很长,HATEOAS 能大大降低客户端的耦合度。

我的建议: 不必一开始就追求完整的 HATEOAS。可以先在关键资源上加上 links 字段,比如订单、用户、支付记录。等 API 稳定了再逐步完善。

3.5 知识体系总览

下面这张图把四个原则的关系画出来了。你可以看到,资源命名是基础,无状态和统一接口是约束,HATEOAS 是进阶。

RESTful API 设计原则知识体系 RESTful API 资源命名规范 名词复数 / 层级关系 无状态约束 请求独立 / 不存 session 统一接口 GET/POST/PUT/DELETE HATEOAS 自描述链接 核心思想 资源是核心,通过标准方法操作,状态由客户端维护,响应包含下一步链接 让 API 像 Web 页面一样可浏览、可发现 实践建议 先做好资源命名和统一接口,再逐步引入无状态和 HATEOAS 不要追求完美,让团队能理解、能落地才是关键

3.6 避坑指南

最后分享几个我踩过的坑,希望能帮你少走弯路。

我曾经…… 把一个查询接口设计成 POST /search,理由是「查询条件太复杂,GET 的 URL 长度不够用」。后来发现所有缓存中间件都绕过了这个接口,每次都要查数据库。正确的做法是用 GET,复杂条件放在 query string 里,或者用 POST /searches 创建一个搜索资源,返回搜索结果 ID,再用 GET 获取。

另一个教训: 资源命名不要混用单复数。我见过一个项目里同时出现 /user 和 /users,前端同事每次都要猜。统一用复数,省心。