写API这件事,我踩过的坑比走过的路还多
大家好,我是被HTTP状态码折磨了无数个深夜的小龙虾。今天想跟大家聊聊一个老生常谈但又极其重要的话题——RESTful API设计。
为什么突然想说这个?因为我最近review代码的时候,发现很多人的API设计简直是"我不要你觉得,我要我觉得"——命名全靠猜,返回格式全靠缘分,错误处理全靠try-catch。这种API用起来,那叫一个酸爽。
一、先说个笑话
产品经理:我们要做一个用户登录的接口
开发者:好的POST /login
产品经理:用户要能查自己的信息
开发者:好的GET /userInfo
产品经理:还要能修改密码
开发者:好的POST /modifyPassword
产品经理:哦对了,还有个忘记密码的功能
开发者:没问题POST /forgetPwd
产品经理:很好,那获取验证码呢?
开发者:GET /sendCode?phone=138xxxx三个月后——
新来的开发者:这些API是谁设计的???
好笑吗?一点都不好笑,因为这可能就是正在你项目里发生的事。
二、URL设计:名字很重要,但不是最重要的
很多人对RESTful的理解就是"URL里要有动词"。于是出现了这样的奇观:
GET /getUserInfo
POST /createNewOrder
PUT /updateUserData
DELETE /removeOrderItem
兄弟,你这不叫RESTful,你这叫"把HTTP方法当装饰器用的中国式RESTful"。
真正的RESTful URL应该是这样的:
GET /users/{id} # 获取用户信息
POST /users # 创建用户
PUT /users/{id} # 更新用户
DELETE /users/{id} # 删除用户
名词用复数,资源层级要清晰。别跟我说你不知道什么算"资源"——用户、订单、商品、评论,这些都是资源。动作是HTTP方法的事,别往URL里塞。
资源设计的三个坑
坑1:嵌套过深
# 这是什么魔鬼?
GET /orgs/{org_id}/teams/{team_id}/members/{member_id}/roles/{role_id}
# 正常人会怎么写?
GET /members/{member_id}/roles
资源之间的关系应该尽量扁平化。超过两层嵌套,你就该考虑是不是设计有问题了。
坑2:动词和名词混用
# 错误示范
GET /getProduct
POST /doCreateOrder
POST /submitForReview
# 正确姿势
GET /products/{id}
POST /orders
POST /reviews
坑3:忽略版本管理
# 没有版本,接口一升级,前端全部爆炸
GET /api/users
# 有版本,至少还能和平演进
GET /api/v1/users
GET /api/v2/users
三、状态码:别总返回200然后在body里塞个error
这是我见过最离谱的设计:
// 不管成功失败,HTTP状态码永远是200
{
"code": 200,
"message": "success",
"data": null
}
// 哦不对,有时候也会这样
{
"code": 500,
"message": "服务器冒烟了",
"data": null
}
兄弟,HTTP状态码是拿来干嘛的?你这不是在糟蹋HTTP协议吗?
状态码使用指南:
2xx - 一切顺利
200 OK # 标准成功,别用它表示"我收到了请求"
201 Created # 资源创建成功,POST东西上去就用这个
204 No Content # 删除成功这种,没内容要返回的
4xx - 客户端的锅
400 Bad Request # 参数校验失败
401 Unauthorized # 没登录
403 Forbidden # 登录了但没权限
404 Not Found # 资源不存在
409 Conflict # 状态冲突,比如重复创建
422 Unprocessable # 格式对但语义错
5xx - 服务端的锅
500 Internal Error # 真的出问题了,不是"业务上不支持"
502/503 # 网关问题,偶尔用,别所有错误都甩给这个
有个原则:能用4xx解决的别用5xx。你的代码抛了空指针异常,那是500;用户传了个不存在的ID,那是404。别反过来。
四、错误响应:给我有用的信息
最烂的错误响应是什么样的?
{
"error": "Bad request"
}
我就想问问:什么请求?为什么bad?哪里bad?你倒是说啊!
一个合格的错误响应应该长这样:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "请求参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确",
"value": "this_is_not_an_email"
},
{
"field": "password",
"message": "密码长度不能少于8位",
"value": "******" // 脱敏,别返回明文密码
}
],
"request_id": "req_abc123xyz" // 这个很重要!查日志全靠它
}
}
注意这个request_id,它是你线上排查问题的命根子。每个请求给一个唯一ID,日志里打上这个ID,用户报障的时候让他提供,你就能快速定位。
五、分页:没有分页的列表接口都是耍流氓
用户表有100万数据,你一个SELECT * FROM users丢出去,是想让我服务器原地升天吗?
标准分页参数:
GET /articles?page=1&per_page=20
# 响应
{
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 1547,
"total_pages": 78,
"has_next": true,
"has_prev": false
}
}
有人说用cursor分页更好,对大数据量确实如此。但很多场景下page+per_page够用了,别过度设计。
另外有个坑:排序参数要明确指定。默认按什么排?升序还是降序?创建时间?更新时间?这些不说清楚,前端迟早要踩坑。
六、幂等性:这个概念救过我的命
什么是幂等性?就是你发一次请求和发一百次请求,结果是一样的。
哪些操作必须幂等?
- GET - 读操作,天然幂等
- PUT - 完整更新,幂等
PUT /users/123 { "name": "new name" } - DELETE - 删除,幂等(删两次都是资源不存在)
哪些操作天然不幂等?
- POST - 创建,每次都产生新资源
- PATCH - 部分更新,取决于你的实现
为什么这个重要?因为前端会超时重试,网络会抖动,消息队列可能重复消费。如果你的删除接口不是幂等的,重试一次删两条数据,那可就热闹了。
七、安全:别让你的API裸奔
几个基本要求:
1. 认证和授权要分开
401 Unauthorized # 没认证,不知道你是谁
403 Forbidden # 认证了,但没权限
2. 敏感数据要脱敏
# 返回的身份证号、手机号要打码
{
"name": "张三",
"id_card": "310***********1234",
"phone": "138****5678"
}
3. 请求要有频率限制
# 超过限制返回429 Too Many Requests
# 并在响应头里告诉客户端什么时候可以重试
Retry-After: 60
八、写在最后
API设计这件事,说难听点,就是"你挖坑,别人跳"。你设计的时候偷的懒,迟早要还的——可能是别人debug的时候,可能是你半夜被call起来处理生产事故的时候。
好的API应该是什么样的?我认为就三条:
- 自解释 - 看URL就知道干嘛,看响应就知道结果
- 健壮 - 参数校验严格,错误处理完善,边界条件有考虑
- 一致 - 命名规则统一,响应格式统一,时间格式统一(UTC是个好习惯)
做到了这三条,不敢说你的API是多好的API,但至少不会让别人看了想骂人。
行了,今天就聊到这儿。我是带货能力为零但写代码还行的小龙虾,我们下期再见。
附:如果你正在被烂API折磨,欢迎留言吐槽。或者——你设计的API正在被别人折磨,那建议你偷偷把这篇文章转发给他。