RESTful API设计:资源命名、状态码规范、分页设计、版本管理、HATEOAS
聊到RESTful API设计,我脑子里第一个蹦出来的词其实是「约定」。说白了,后端API就是前后端之间的合同。合同写得含糊,后面全是扯皮。我在项目里见过太多因为API设计不规范导致的返工,嗯,今天咱们就把这块掰开揉碎了讲清楚。
核心原则:RESTful API不是写代码,是设计资源。你设计的每一个端点,都应该是对资源的操作,而不是对功能的调用。
1. 资源命名:别让URL变成函数名
我个人习惯把资源命名当成数据库表名来思考。名词、复数、小写、连字符分隔——这是铁律。
// 好的命名
GET /api/users
GET /api/users/123
GET /api/users/123/orders
POST /api/users
// 糟糕的命名
GET /api/getUserList
GET /api/user?id=123
POST /api/createNewUser
GET /api/userOrders?userId=123
为什么不能用动词?你想想看,HTTP方法本身就是动词。GET就是获取,POST就是创建,PUT就是全量更新,PATCH就是部分更新,DELETE就是删除。你在URL里再写一遍动词,等于说废话。
我曾经在一个遗留项目里看到过 /api/deleteUserById 这种端点,结果前端传参传错了,删了不该删的数据。嗯,从那以后我对命名规范格外敏感。
| HTTP方法 | 资源路径 | 语义 |
|---|---|---|
| GET | /users | 获取用户列表 |
| GET | /users/42 | 获取单个用户 |
| POST | /users | 创建用户 |
| PUT | /users/42 | 全量更新用户 |
| PATCH | /users/42 | 部分更新用户 |
| DELETE | /users/42 | 删除用户 |
小技巧:资源嵌套不要超过三层。超过三层就该考虑是不是设计有问题了。比如 /users/123/orders/456/items/789 这种,建议拆成 /orders/456/items/789 加上用户ID作为查询参数。
2. 状态码规范:别什么都返回200
我见过最离谱的事,是有人把所有请求都返回200,然后在响应体里塞一个 {"code": 1, "message": "error"}。这等于把HTTP协议当传输层用,自己重新造了一套状态码。何必呢?
HTTP状态码已经定义得很清楚了,直接用就好:
- 200 OK:GET成功、PUT成功、PATCH成功
- 201 Created:POST创建资源成功
- 204 No Content:DELETE成功,不需要返回内容
- 400 Bad Request:参数校验失败、请求体格式错误
- 401 Unauthorized:未认证,比如没传token
- 403 Forbidden:已认证但没权限
- 404 Not Found:资源不存在
- 409 Conflict:资源冲突,比如重复创建
- 422 Unprocessable Entity:语义错误,比如邮箱格式不对
- 500 Internal Server Error:服务端异常
注意:别滥用500。很多团队把参数校验失败也返回500,这是不对的。客户端传错参数是客户端的锅,应该返回4xx。只有服务端自己炸了才用5xx。
3. 分页设计:别让用户一次拿太多
不分页的API,说白了就是定时炸弹。数据量小的时候没事,等用户量上来了,一次查询几万条记录,数据库扛不住,网络带宽也扛不住。
我个人推荐基于游标的分页,而不是传统的页码分页。为什么?
- 页码分页:
GET /users?page=2&size=20。简单直观,但有个致命问题——如果中间有人插入了数据,翻页时会出现重复或遗漏。 - 游标分页:
GET /users?cursor=eyJpZCI6MTAwfQ==&limit=20。基于上次返回的最后一条记录的ID,性能稳定,不会受数据变动影响。
// 游标分页响应示例
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ==",
"has_more": true,
"total": 1000
}
}
我曾经在一个电商项目里用过页码分页,结果用户在下单时翻页,后台新增了商品,导致用户看到重复数据,体验极差。后来全改成游标分页,再没出过问题。
建议:如果数据量不大(比如几千条),页码分页也够用。但如果你做的是社交、电商这类高频写入的系统,游标分页是更稳妥的选择。
4. 版本管理:别让旧客户端炸掉
API一旦发布出去,你就失去了对客户端的完全控制。用户可能用着三年前的App,你总不能强制所有人升级吧?
版本管理有三种常见方式:
- URL路径版本:
/api/v1/users、/api/v2/users。最直观,但会污染URL。 - 请求头版本:
Accept: application/vnd.myapp.v1+json。更符合RESTful风格,但调试起来麻烦。 - 查询参数版本:
/api/users?version=1。简单但容易被忽略。
我个人习惯用URL路径版本。虽然不够「纯粹」,但胜在直观。前端看一眼URL就知道调的是哪个版本,后端路由也容易处理。
// Ktor中版本路由示例
routing {
route("api/v1") {
get("/users") { /* v1逻辑 */ }
}
route("api/v2") {
get("/users") { /* v2逻辑 */ }
}
}
避坑指南:我曾经见过一个团队,版本号直接写在代码里,每次发版都要改代码。正确的做法是把版本号作为配置项,通过环境变量或配置文件注入。这样同一个代码包可以部署成v1和v2两个服务。
5. HATEOAS:让API自己「说话」
HATEOAS全称是「Hypermedia as the Engine of Application State」。名字很唬人,说白了就是:API响应里带上链接,告诉客户端下一步能做什么。
举个例子,你查询一个订单,响应里不光有订单数据,还有「支付」「取消」「查看物流」这些操作的链接。客户端不需要硬编码URL,跟着链接走就行了。
{
"order_id": "ORD-2024-001",
"status": "pending_payment",
"total": 299.00,
"_links": {
"self": { "href": "/api/v1/orders/ORD-2024-001" },
"pay": { "href": "/api/v1/orders/ORD-2024-001/payments", "method": "POST" },
"cancel": { "href": "/api/v1/orders/ORD-2024-001", "method": "DELETE" }
}
}
这样做的好处是什么?客户端不需要知道业务逻辑。订单状态变了,链接也跟着变。比如订单已经支付了,响应里就不会再出现「pay」链接,而是出现「refund」链接。客户端只需要渲染这些链接就行。
嗯,说实话,HATEOAS在实际项目中用得不算多。因为大部分前端框架还是习惯硬编码URL。但如果你做的是开放平台,给第三方开发者用,HATEOAS能大大降低对接成本。
我的建议:不用全盘HATEOAS,但可以在关键资源上加上链接。比如订单详情、用户资料这些经常被引用的资源,带上关联资源的链接,能让API更「自解释」。
知识体系总览
下面这张图把RESTful API设计的五个核心维度串起来了。你可以把它当成设计API时的检查清单:
这五个维度,说白了就是API设计的「五件套」。资源命名决定了API好不好读,状态码决定了API好不好调,分页决定了API能不能扛住量,版本管理决定了API能不能持续迭代,HATEOAS决定了API能不能自我解释。
我在项目里见过只做前两点的团队,也见过五件套全上的团队。说实话,没有绝对的对错,关键看你的业务场景。但有一点是确定的——规范越早定越好。等API已经上线了再改,那成本可就高了。
总结一句话:好的API设计,是让调用者不用看文档也能猜个八九不离十。如果你设计的API需要别人反复来问你「这个参数是什么意思」,那说明设计还有优化空间。
公众号:蓝海资料掘金营,微信deep3321