第3章 设计RESTful API的最佳实践
3.1 为什么需要一套API设计规范?
说实话,我早年刚接手后端项目时,团队里连个统一的API风格都没有。有人用/getUser,有人用/users/1,还有人搞/api/v1/queryUser?id=1。前端同学对接接口基本靠问,文档写出来都认不全。这种混乱,本质上就是缺少一套RESTful的设计规范。
RESTful API不是银弹,但它提供了一套清晰的语言。资源用名词,动作用HTTP方法,状态用标准状态码。团队一聊就懂,前端收到响应就能处理。我在项目中见过很多团队因为API设计不统一,后期联调耗费了大量精力。所以从第一行代码开始,我建议你定好规则。
核心原则:把API当作资源系统的门面来设计。路径只表示资源,动作交给HTTP动词。
3.2 资源命名与URL结构
URL是API的名片。你想想看,一个阅读体验好的API,路径一眼就能看懂它在干什么。
- 使用名词复数:
/users、/orders,而不是/getUser或/user。 - 层级结构用斜杠:
/users/1/orders表示用户1的订单列表。 - 避免动词:不需要
/createOrder,直接用POST /orders。 - 用连字符代替下划线:
/order-items比/order_items更友好。 - 查询参数用于过滤和排序:不要塞进路径里,比如
/users?status=active&sort=created_at。
| 不推荐 | 推荐 | 说明 |
|---|---|---|
| /getUser?id=1 | /users/1 | HTTP GET 已包含“获取”语义 |
| /createOrder | POST /orders | POST 就是创建 |
| /deleteUser/1 | DELETE /users/1 | DELETE 就是删除 |
| /updateUser/1 | PUT /users/1 | PUT 或 PATCH 用于更新 |
我个人习惯在路径中全部小写,用连字符分隔单词。比如/system-logs,而不是/systemLogs。这样在大小写敏感的服务器上少踩坑。
3.3 HTTP方法与幂等性
HTTP方法不是随便选的。它们背后有幂等性的逻辑。你想想看,如果前端因为网络重试发出了两次POST /orders,结果生成了两个订单,这谁能忍?
- GET:查询,幂等。多次调用结果不变。
- POST:创建,不幂等。每次调用都可能产生新资源。
- PUT:完整替换,幂等。客户端发全量数据。
- PATCH:部分更新,不幂等(取决于实现)。
- DELETE:删除,幂等。第一次返回200,第二次返回404,但资源状态已变。
# 示例:用户订单API
GET /users/1/orders # 获取用户1的订单列表
POST /users/1/orders # 为用户1创建新订单
GET /users/1/orders/5 # 获取订单5详情
PUT /users/1/orders/5 # 完整替换订单5
PATCH /users/1/orders/5 # 部分更新订单5
DELETE /users/1/orders/5 # 删除订单5
我曾经在一个项目里,团队用POST做幂等的资源替换,导致前端重试后数据被覆盖了多次。后来我们统一改用PUT,并在请求头加上Idempotency-Key,才解决了这个问题。
3.4 状态码的正确使用
很多新手喜欢所有成功都返回200,错误全部返回500。这样前端得自己解析body才知道具体发生了什么——其实完全没必要。
- 200 OK:GET、PUT、PATCH成功。
- 201 Created:POST成功创建资源。
- 204 No Content:DELETE成功,或不需要返回body的操作。
- 400 Bad Request:参数非法、格式错误。
- 401 Unauthorized:未认证或认证过期。
- 403 Forbidden:已认证但无权限。
- 404 Not Found:资源不存在。
- 409 Conflict:资源冲突(如重复创建、版本冲突)。
- 422 Unprocessable Entity:语义错误,比如校验失败。
- 429 Too Many Requests:限流。
- 500 Internal Server Error:服务器内部错误,不要滥用。
| 操作 | 成功 | 失败 |
|---|---|---|
| GET /users | 200 + 用户列表 | 400/401/500 |
| POST /users | 201 + 新用户数据 | 400/409/422 |
| DELETE /users/1 | 204 无内容 | 401/404/500 |
状态码是API的语义边界。前端只需要看状态码就能决定走哪个分支,不需要解析body里的error code。这样客户端代码会简洁很多。
3.5 版本管理
API上线后一定会变。不加版本管理,升级就是灾难。我见过直接把旧路由改掉,结果客户端全部报404的惨案。
- URL路径版本:
/v1/users、/v2/users。最直观,方便调试。 - 请求头版本:
Accept: application/vnd.myapi.v2+json。更干净,但调试工具未必好设。 - 查询参数版本:
/users?version=2。容易与普通参数混淆,不推荐。
我个人偏爱URL路径版本。虽然有人觉得URL里带版本号不RESTful,但它对前端和客户端最友好。打开浏览器就能测试,curl也方便。
# URL路径版本示例
GET /v1/users # 老版接口
GET /v2/users # 新版接口,可能新增字段或改变行为
3.6 错误响应体统一
错误发生时,只返回一个状态码是不够的。前端需要知道哪里错了、怎么改。我建议统一一个错误结构:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "未找到该用户",
"details": "用户ID: 42 不存在",
"timestamp": "2025-03-25T10:30:00Z"
}
}
- code:机器可读的错误码,客户端用来做逻辑分支。
- message:人类可读的错误描述,可显示给用户。
- details:调试信息,生产环境可酌情隐藏。
- timestamp:便于追踪问题发生时间。
记住:错误响应也要遵循相同的Content-Type。别在成功时返回JSON,失败时返回纯文本,前端解析会炸。
3.7 分页、过滤与排序
列表接口如果不用分页,一次返回10万条数据,服务端和客户端都扛不住。我在项目中习惯用一种设计:
- 分页:使用
?page=2&per_page=20,返回中附带总条数和总页数。 - 过滤:使用查询参数,如
?status=active。 - 排序:使用
?sort=created_at或?sort=-created_at(负号表示降序)。
// 响应示例
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 20,
"total": 150,
"total_pages": 8
}
}
3.8 认证与授权
RESTful API通常无状态,所以认证信息得放在每个请求里。常见的做法:
- Bearer :放在
Authorization: Bearer <>头里。 - :放在Header或查询参数中,适合服务间调用。
- OAuth 2.0:适合第三方授权。
千万别把密码直接放在URL或body里。我之前接手过一个老项目,他们用/login?user=admin&pass=123456,我这个汗啊……
警告:不要在GET请求的body中放认证信息。GET请求可能被浏览器缓存、被日志记录,直接泄漏。
3.9 总结一下
设计RESTful API没有的对错,但遵守一套共识能让你少踩很多坑。我从资源命名、HTTP方法、状态码、版本管理、错误响应、分页认证几个维度讲了核心要点。你把这些规则定好,写进团队的API规范文档里,联调效率至少提升一倍。
说白了,好的API设计是让调用方感觉“这接口就该这么用”。用最短的时间讲清楚业务,用最少的字段完成任务,这就是我的追求。