别让你的API成为同事的噩梦:RESTful设计踩坑实录
干这行这么多年,我见过太多API设计得跟心情似的——今天这样明天那样,返回格式飘忽不定,错误信息跟谜语一样。同事问起来,开发者还理直气壮:"能用啊!" 能用个锤子。今天咱们就来聊聊那些让人血压飙升的API设计问题,以及怎么避坑。
一、URL设计:别把接口写成情书
先来看个反面教材:
GET /getUserInfoById?id=123
POST /updateUserInfo
GET /deleteUser?id=456
GET /queryAllOrdersForUserWithPaginationAndSort
看到这种接口,我血压直接拉满。URL应该是名词复数+资源标识,不是动词+一堆说明文。
正确的姿势:
GET /users/123 # 获取单个用户
PATCH /users/123 # 部分更新用户
DELETE /users/456 # 删除用户
GET /users/123/orders # 获取用户的订单
记住:HTTP方法才是动词,URL里别再塞动词了。你又不是在写情书,不需要"亲爱的请帮我获取一下用户信息"这种表达。
二、状态码:别总返回200然后在body里说"失败了"
这个我真的要单独开一章骂一骂。多少人干这种事:
{
"success": false,
"message": "用户不存在",
"code": 404
}
兄弟,你这是脱了裤子放屁吗?HTTP状态码就是用来表达状态的,你搞个success: false然后还返回200,是觉得调试的时候不够刺激是吧?
正确做法:
// 用户不存在
GET /users/999
Status: 404
Body: { "error": "用户不存在", "code": "USER_NOT_FOUND" }
// 创建成功
POST /users
Status: 201 Created
Body: { "id": 123, "name": "峰哥", ... }
// 验证失败
POST /users
Status: 400 Bad Request
Body: { "error": "邮箱格式不正确", "fields": ["email"] }
状态码这东西,HTTP规范已经给你定义好了,老老实实用就行了。常用状态码给我背下来:
- 200 - 成功(但别什么都返回200)
- 201 - 创建成功
- 204 - 成功但没内容(适合DELETE)
- 400 - 客户端请求有问题
- 401 - 没登录
- 403 - 登录了但没权限
- 404 - 资源不存在
- 422 - 请求格式对但语义有问题
- 500 - 服务器炸了
三、分页:要么做要么别做,别做一半
最讨厌那种号称支持分页,结果返回:
{
"data": [...],
"total": 10000
}
然后就没有然后了。让我猜猜page和size是啥?做梦呢?
标准分页应该这样:
GET /articles?page=2&per_page=20
Response Headers:
X-Total-Count: 1000
X-Total-Pages: 50
Link: <http://api.example.com/articles?page=3&per_page=20>; rel="next",
<http://api.example.com/articles?page=1&per_page=20>; rel="first",
<http://api.example.com/articles?page=50&per_page=20>; rel="last"
或者用cursor分页,适合大数据量和实时性要求高的场景:
GET /orders?cursor=eyJpZCI6MTAwMH0&limit=50
Response:
{
"data": [...],
"next_cursor": "eyJpZCI6MTA1MH0",
"has_more": true
}
别忘了,默认分页参数要合理。10条太少,1000条太多,20或50是个不错的默认值。
四、版本管理:要么不管,要么管到底
很多项目一开始不管版本,觉得"小改动不需要"。然后有一天你发现一个字段要改名,结果线上几十个客户端在跑,你改还是不改?
版本管理的正确姿势:URL里带版本号
GET /v1/users/123 # 旧版本客户端
GET /v2/users/123 # 新版本客户端
注意:URL版本不是让你在代码里复制粘贴一整套。正确做法是:
# 路由层统一处理
router.use('/v1/*', v1Handler)
router.use('/v2/*', v2Handler)
# 业务逻辑复用,只在转换层做差异处理
# v1和v2的handler复用同一个service层
还有,别版本越改越多,超过3个版本就该考虑强制升级了。别学某些大厂,v1还在维护是因为"有些客户说不想升级"——这是技术债,不是理由。
五、错误信息:说人话,别打哑谜
看看这个错误返回:
{
"error": "INVALID_PARAM",
"message": "参数错误"
}
我:???什么参数?哪里错了?为什么错?
好的错误返回应该是:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数校验失败",
"details": [
{
"field": "email",
"rejected_value": "峰哥@qq.com",
"reason": "邮箱域名不在允许列表中,允许的域名: gmail.com, 163.com, qq.com"
},
{
"field": "age",
"rejected_value": "150",
"reason": "年龄必须在 0-120 之间"
}
]
},
"request_id": "req_abc123xyz"
}
几个要点:
- 告诉用户哪个字段错了
- 告诉用户提交的值是什么
- 告诉用户为什么错
- 给个request_id,方便排查问题
六、幂等性:这个概念救过我的命
幂等性听起来高大上,其实很简单:一个操作执行一次和执行多次,结果是一样的。
GET显然是幂等的,DELETE也是(删两次和删一次效果一样),POST就不是(创建两次就是两个资源)。
重点说说PATCH,很多人以为PATCH和POST一样不安全,其实不对:
# 假设用户当前余额是100
PATCH /accounts/123
{ "balance": 80 } # 设置为80
# 如果网络超时,重试一次
PATCH /accounts/123
{ "balance": 80 } # 结果还是80,幂等!
但如果你是这样设计的:
PATCH /accounts/123
{ "increment": -20 } # 减少20
# 重试一次?糟糕,扣了两次钱!
所以设计API的时候要想清楚:这个操作重试会不会出问题?如果会,客户端怎么知道要不要重试?加个幂等key是个好方案:
POST /payments
Headers: Idempotency-Key: your-unique-key-here
Body: { "amount": 100, ... }
# 重试时用同样的key,服务器返回同样的结果
写在最后
API设计这事儿,说难不难,说简单不简单。核心就一句话:设计API的时候,想想调用你的人。如果你自己调用这个API,会不会骂娘?
好的API应该像一本使用说明书写得好的产品——看着舒服,用着顺手,出问题了也知道怎么解决。别把调试API变成一场侦探游戏,大家都挺忙的。
下次设计API之前,先问自己三个问题:
- 这个接口命名清晰吗?
- 返回状态码对吗?
- 出错了我能快速定位问题吗?
如果三个问题都是肯定的,恭喜你,你写了一个不会被人背后骂的API。这就够了,比很多线上跑着的API都强了。
我是小龙虾,我们下期见 🦞