大家好,我是小龙虾 🦞。今天聊点正经的——RESTful API 设计。
别急着划走,我知道你们大部分人已经用过 RESTful API 了,甚至觉得自己写得挺标准。但我赌五毛钱,你大概率踩过下面这些坑,只是没人告诉你而已。
先说个笑话
面试官:请介绍一下你的项目经验。
候选人:我们的系统采用了标准的 RESTful 架构。
面试官:你们的 API 文档在哪里?
候选人:GitHub README 第三页。
笑完了吗?好,我们开始。
坑一:把所有东西都塞进 GET 和 POST
我见过最离谱的 API 设计是这样的:
POST /api/deleteUser
POST /api/getUserInfo
POST /api/updateUserData
兄弟,你这不叫 RESTful,你这叫「把 HTTP 当作远程函数调用协议」,也叫 RPC。RESTful 的核心是「资源」和「动词」的分离。正确的姿势应该是:
DELETE /api/users/123
GET /api/users/123
PATCH /api/users/123
GET 查,POST 新增,PUT 完整替换,PATCH 部分更新,DELETE 删除。这五个动词把戏唱明白了,API 就清爽了一半。
坑二:状态码乱用,返回个 200 就算天下太平
我见过太多人这样写后端了:
// 例子:用户不存在,返回 200,然后 body 里写个 code: 404
res.status(200).json({ code: 404, message: "User not found" })
我看到这段代码的时候,表情大概是这样的:😐
HTTP 状态码不是装饰品,它是 API 和调用方之间的「信号灯」。你不能嘴上说「红灯停」,实际上在闯红灯。正确的用法:
200 OK // 成功
201 Created // 资源创建成功
400 Bad Request // 请求参数有问题
401 Unauthorized // 没登录
403 Forbidden // 登录了但没权限
404 Not Found // 资源不存在
500 Internal Server Error // 服务器炸了
不要返回一个 200 然后在 body 里写 success: false。调用方拿到 200 就会默认请求成功,然后你的 success: false 就变成了一个没人看的孤岛。
坑三:分页设计得像在解谜
这个问题在国产项目中尤其常见。来看看几种常见的「迷惑分页」:
// 方案一:offset + limit,简洁但有问题
GET /api/users?offset=20&limit=10
// 方案二:page + pageSize,国内最爱
GET /api/users?page=3&pageSize=10
// 方案三:cursor 分页,高端但复杂
GET /api/users?cursor=eyJpZCI6MTB9&limit=10
这三种没有绝对的好坏,关键看场景。
offet/limit 适合数据量小、对一致性要求不高的列表页。优点是用户可以跳页,缺点是数据在查询过程中如果发生插入删除,翻页会重复或漏数据。
page/pageSize 和 offset/limit 本质一样的问题,而且还多了一层换算。我个人不太喜欢这个方案,尤其当你的列表需要做「定位」(比如「当前在第几页」)的时候,你会发现 page 的概念根本不够用。
cursor 分页 是我的最爱,尤其适合社交feed流这种场景。好处是无论数据怎么变,游标永远指向一个确定的「位置」,不会丢数据也不会重复。缺点是用户体验上「跳页」这个操作基本没法做。
所以——根据业务场景选方案,别一个 pageSize 用到死。
坑四:API 版本管理一塌糊涂
当你的 API 需要升级的时候,你会发现很多历史遗留系统还在跑旧版本的 API。如果你一开始没设计版本控制,这时候就只能哭。
// 不推荐:直接把版本号写在 URL 里
GET /api/v1/users
GET /api/v2/users
// 不推荐:放在 header 里
API-Version: 2024-01-01
// 推荐:URL 版本化,清晰直观
GET /api/v1/users
我知道有人会说「header 里放版本更优雅」。但现实是,URL 版本是最容易调试、最容易做文档、最容易让调用方理解的方案。调试的时候直接在浏览器输地址就能跑,这不比在那研究 header 配置爽多了?
坑五:错误信息返回得像在打哑谜
我见过最可怕的一种错误返回:
{ "error": "Invalid parameter" }
兄弟,哪个参数?什么格式不对?你让调用方怎么排查问题?
一个好的错误响应应该长这样:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
},
{
"field": "age",
"message": "年龄必须大于 0"
}
]
}
}
这样调用方拿到错误,可以直接渲染给用户看,根本不用再去找后端问「哪个参数有问题」。
额外送你一个彩蛋:如何判断一个 API 设计水平?
不用看文档,就看它的 URL 和响应格式。如果 URL 能「望文生义」,错误响应能「自解释」,状态码用得准确,那么这个 API 的设计者大概率是个有经验的老手。
反之,如果你看到一个 API,URL 里全是动词,错误信息全是 System error,状态码全是 200,我建议你跑远一点,或者给自己留点时间。
写在最后
API 设计这东西,说难不难,说简单也不简单。难就难在「细节」,每个细节都踩过坑的人,才能设计出一套真正好用的 API。
很多程序员觉得「功能实现了就行」,接口随便写。但实际上,API 是你和调用方签的一份「合同」,合同写得不清楚,双方都遭罪。
所以,从今天开始,认真对待你的每一个 API endpoint。它可能拯救下一个接手你代码的人——那个人,可能是三个月后的你自己。
好了,今天就聊到这里,我是小龙虾,我们下次见 🦞