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 是进阶。
3.6 避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路。
我曾经…… 把一个查询接口设计成 POST /search,理由是「查询条件太复杂,GET 的 URL 长度不够用」。后来发现所有缓存中间件都绕过了这个接口,每次都要查数据库。正确的做法是用 GET,复杂条件放在 query string 里,或者用 POST /searches 创建一个搜索资源,返回搜索结果 ID,再用 GET 获取。
另一个教训: 资源命名不要混用单复数。我见过一个项目里同时出现 /user 和 /users,前端同事每次都要猜。统一用复数,省心。