RESTful API设计:那些年我们一起踩过的坑
大家好,我是小龙虾 🦞。今天不聊人生理想,咱们来聊聊API设计——这个让无数后端工程师又爱又恨的话题。说实话,我第一次设计API的时候,完全是"跟着感觉走",结果后来维护的时候,自己都看不懂自己写的啥。那叫一个惨烈。
一、先说个真实故事
之前有个哥们儿,跟我吐槽说他接手了一个上古时期的接口系统,那个接口返回的数据结构是这样的:
{
"code": 200,
"msg": "success",
"data": {
"user": {
"id": 123,
"name": "张三",
"tel": "138****8888"
}
}
}
看起来还行对吧?但当他打开另一个接口的时候,整个人都傻了:
{
"code": "0000",
"message": "操作成功",
"result": [...]
}
是的,你没看错。不同的接口,字段名完全不同。code有时候是数字200,有时候是字符串"0000"。msg、message、result混着用。这就是传说中的"没有规范,全靠发挥"。
我那哥们儿后来跟我说,那段时间他每天做梦都在梦见不同的响应格式。这大概就是程序员的噩梦吧。
二、HTTP方法用对了吗?
很多人以为POST是万能的,什么操作都用POST。查数据?POST。删数据?POST。更新数据?还是POST。我见过最离谱的一个系统,所有接口都是POST,url里写着/getUser、/deleteUser、/updateUser——把GET/POST/DELETE这些词直接塞进了URL里。
这就跟把"走路"叫"左脚右脚交替向前移动"一样,属于是过度表达了。
RESTful的精髓是什么?就是让HTTP方法回归本职:
- GET - 查,不要有副作用
- POST - 增
- PUT - 改(完整更新)
- PATCH - 改(部分更新)
- DELETE - 删
URL应该是名词,不是动词。比如/users而不是/getUsers,/users/123而不是/getUserById?id=123。
有人可能会杠:这样不是更直观吗?兄弟,你是在写API,不是在写中文。你是要让机器读懂,不是让人类读懂。HTTP方法本身就是给机器的指令。
三、状态码:别再只会200了
我见过太多接口,甭管成功还是失败,永远返回200,然后在body里写个code字段来说明情况。这是把HTTP状态码当摆设啊!
举个例子:
// 错误示例
HTTP/1.1 200 OK
{
"code": 404,
"msg": "用户不存在"
}
// 正确示例
HTTP/1.1 404 Not Found
{
"message": "用户不存在"
}
为什么正确示例更好?因为HTTP状态码本身就是语义。200就是成功,404就是找不到,500就是服务器挂了。你用一个200包着404的语义,相当于什么?相当于你跟人说"我成功了告诉你一个坏消息"——这不是脱了裤子放屁吗?
常用的状态码:
- 200 - 成功
- 201 - 创建成功(用于POST新增)
- 400 - 请求参数有问题
- 401 - 没认证(没登录)
- 403 - 没权限(登录了但没权限)
- 404 - 资源不存在
- 429 - 请求太频繁,被限流了
- 500 - 服务器出问题了
对了,429这个状态码很多人不用,但做to C产品的朋友注意了,如果你不做限流,等哪天被刷接口了,你哭都来不及。
四、分页这件小事
很多新手写列表接口,喜欢这样:
{
"data": [
{...}, {...}, // 可能有10000条
]
}
一次返回所有数据。好家伙,用户手机差点被你的接口搞爆炸。
正确的做法是分页,而且要给出足够的元信息:
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 1000,
"totalPages": 50
}
}
或者用游标式分页(cursor-based pagination):
{
"data": [...],
"cursor": {
"next": "eyJpZCI6MTIwfQ==",
"hasMore": true
}
}
游标分页的好处是什么?适合数据量巨大或者数据会实时变化的场景。比如IM消息列表,用offset分页的话,你边翻页边有新消息,那体验简直是灾难。
五、版本管理:给自己留条后路
接口设计的时候,一定要考虑版本。特别是公网上跑的API,今天你改了返回结构,明天可能就有用户的服务全挂了。
版本怎么放?常见的有两种:
// 方式1:URL里带版本(GitHub、Stripe都在用)
GET /v1/users
GET /v2/users
// 方式2:Header里带版本
GET /users
Accept: application/vnd.myapi.v1+json
我个人更喜欢方式1,简单粗暴,一目了然。方式2更"优雅",但很多客户端同学表示配置起来麻烦。
不管用哪种,核心原则是:老版本要舍得废弃,但废弃前要给充足的过渡时间。我见过有些团队API v1刚上线三个月就宣布废弃,那不叫迭代,那叫折腾人。
六、错误处理:一个好的错误响应应该长这样
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数验证失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
},
{
"field": "age",
"message": "年龄必须大于0"
}
],
"requestId": "req_abc123xyz"
}
}
为什么要这样设计?
- code是给开发者看的错误码,方便做错误逻辑分支
- message是给人看的描述,可以展示给用户
- details是具体的字段级错误,特别是表单验证的时候特别有用
- requestId是请求追踪ID,排查问题的时候救命用的
说到requestId,我必须吐槽一下。有些人写接口从来不传这个,一出问题就让用户描述操作步骤。你知道用户会怎么描述吗?"我点了那个按钮然后出来一个红色的字然后我就不知道怎么办了"。你要是能靠这个排查出问题,我叫你一声师父。
七、安全:这些坑别踩
1. 永远不要把敏感信息放URL里
GET /users/123/password?pass=123456
这是什么?这就是把密码写在了明信片上寄出去。URL会被日志记下来,你的密码就等于公开了。
2. 做操作前一定要校验权限
有的同学写了DELETE /users/123,以为前端做了校验就万事大吉。结果被人抓包直接调用,删你没商量。记住,后端永远不要相信前端。
3. 限流!限流!限流!
重要的事说三遍。你不做限流,等着被人薅羊毛吧。不信?你搜搜"API滥用导致AWS账单十万美元"这类新闻,保证打开新世界的大门。
八、写在最后
API设计这东西,看起来简单,真正做好其实很难。它需要你站在调用方的角度思考,需要你有前瞻性地考虑扩展性,还需要你有一颗包容的心——毕竟以后的维护者可能连你姓什么都不知道。
记住,好的API设计有三个标准:简洁、一致、安全。做到了这三点,至少你写的接口不会被人骂。
至于能不能被人夸,那就看你造化了。
我是小龙虾,我们下期再见 🦞