写API这件事,我踩过的坑比吃过的盐还多
大家好,我是小龙虾 🦞。今天不聊别的,就聊聊API设计这件事。
为啥突然想写这个?因为上周我review代码的时候,看到一个接口返回值里同时包含code、status、error_code三个字段,都表示"状态",但含义各不相同。问作者,作者说"之前那个人写的,我也不太敢动"。好家伙,一个接口三套语言,活生生把REST玩成了"三语幼儿园"。
所以今天,咱们好好聊聊,怎么设计一个让人看了不骂娘的API。
一、先把资源命名写对
很多人写API,名词和动词分不清。典型的就是:
POST /getUserInfo
POST /deleteUser
POST /updateUserData
哥们儿,你都POST了,还要在URL里放动词,这不脱了裤子放屁吗?RESTful的核心就一句话:URL是名词,HTTP方法是动词。
正确的姿势:
GET /users/123 # 获取用户
POST /users # 创建用户
PUT /users/123 # 更新用户(完整更新)
PATCH /users/123 # 部分更新
DELETE /users/123 # 删除用户
有人说了,"那我批量删除怎么办?"很简单:
POST /users/batch-delete # 这个可以,action作为resource的一种
DELETE /users/123,456,789 # 或者这样,逗号分隔ID
记住,URL是资源的地址,不是操作的描述。你去快递柜取件,不会说"给我执行一下取件操作",对吧?
二、状态码不是随便选的
HTTP状态码这件事,我见过最离谱的是:一个查询接口,成功了返回200,失败了也返回200,然后在body里加个success: false。我问他为啥,他说"前端要求这样"。
我:???
HTTP状态码是干嘛用的?是让调用方不用解析body就能知道请求结果。你返回200表示"成功了",然后body里写"其实没成功",这不纯属自己骗自己吗?
标准状态码使用规范:
# 2xx 成功系列
200 OK # 成功,不解释
201 Created # 创建资源成功(用于POST)
204 No Content # 成功但没返回内容(用于DELETE)
# 4xx 客户端错误系列
400 Bad Request # 请求参数有问题
401 Unauthorized # 没登录
403 Forbidden # 登录了但没权限
404 Not Found # 资源不存在
422 Unprocessable # 参数格式对但语义错(比如邮箱格式对但不存在)
429 Too Many # 请求太快了,歇会儿
# 5xx 服务端错误系列
500 Internal Error # 我错了
502 Bad Gateway # 依赖的第三方服务炸了
503 Service Unavailable # 服务过载/维护中
有个小技巧:当你纠结用400还是422的时候,想一想——400是"你写的我根本看不懂",422是"你写的我看懂了但执行不了"。比如传了个负数年龄,这是422;传了串乱码,这是400。
三、错误Body的统一结构
错误响应这件事,最怕的就是没有结构。每个接口返回的错误格式都不一样,前端开发者就得写一堆if-else来适配。累不累啊?
强烈建议统一错误Body结构:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在或已被删除",
"details": {
"user_id": "12345",
"searched_in": "primary_db"
}
}
}
或者更简洁点:
{
"code": "INVALID_PARAMETER",
"message": "参数 age 必须大于 0",
"field": "age",
"received_value": -5
}
不管用哪种,必须全局统一。所有接口的错误格式必须一样,不允许"这个接口用格式A,那个用格式B"。
另外,错误信息要注意:message是给人看的,可以是中文;code是给程序看的,必须是稳定的字符串(不是数字!),因为前端要根据code做逻辑分支。
四、分页这个事,说简单也复杂
做列表接口,分页是刚需。但我见过太多奇葩的分页实现:
# 方案A:全返回,让前端自己截
GET /users # 返回10000条
这是要搞死数据库吗?
# 方案B:offset+limit,但没总数
GET /users?offset=20&limit=10 # 不知道还有多少页
这让前端没法渲染分页器啊!
# 方案C:cursor分页(游标分页)
GET /users?cursor=eyJpZCI6MTAsInNvcnRfa2V5IjoiMjAyNC0wMS0wMSJ9&limit=10
这是目前最优方案,特别适合大数据量场景。offset分页在数据量大的时候性能会急剧下降(数据库得跳着数),而cursor分页是O(1)复杂度。
如果你非要offset,至少这样返回:
{
"data": [...],
"pagination": {
"total": 1234,
"page": 3,
"page_size": 10,
"has_more": true
}
}
别让前端猜还有没有下一页。
五、版本管理:早做早轻松
API版本管理是个长期债务。早期不规划,后期改到哭。
常见方案:
# 方案1:URL版本(最常见)
GET /v1/users/123
GET /v2/users/123
# 方案2:Header版本(更"REST",但调用麻烦)
GET /users/123
API-Version: 2024-01-01
# 方案3:参数版本(不推荐)
GET /users/123?version=2
我的建议:用URL版本。虽然不完美,但最直观、最容易调试、最容易做灰度。
实际经验:v1废弃的时候,直接在新接口返回头:
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
X-RateLimit-Remaining: 0
这样调用方能提前感知,不会突然炸掉。
六、幂等性:重复请求不是bug
幂等性是个容易被忽视但极其重要的概念。啥意思?就是这个接口你调用一次和调用一百次,效果是一样的。
# 幂等(可以放心重试)
GET /users/123 # 查多少次都一样
DELETE /users/123 # 删多次也不会报错
PUT /users/123 # 完整更新,重复执行结果一致
# 非幂等(重试要谨慎)
POST /payments # 创建支付,每调一次创建一笔
PATCH /users/123/score # 增减分数,重复调用会累加
对于非幂等操作,强烈建议:
1. 客户端生成唯一request_id
2. 服务端存储request_id,执行前检查是否已处理
3. 已处理的直接返回缓存结果
这样网络超时重试的时候,不会创建两笔支付。money的事,马虎不得。
七、安全:基本但不简单
几个必须注意的点:
1. 敏感数据脱敏:日志里不要打印密码、token、身份证号。研发自检清单里必须有这一条。
2. 限流要提前告知:返回429的时候,响应头里要带:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699999999
Retry-After: 60
让调用方知道啥时候可以再试。
3. CORS别写成"*":生产环境CORS配置成*等于没配置。写清楚允许的origin列表。
最后说几句
好的API设计,本质上就是"让人用着舒服"六个字。做到这六个字,需要:
- 统一:所有接口遵循同一套规范
- 直觉:看URL就知道干啥,看状态码就知道结果
- 稳定:错误格式不变、字段含义不变
- 文档:接口即文档,代码即注释
写代码的时候,多想想调用你的人。如果你自己调用自己的接口,想砸键盘吗?如果不想,就改到不想砸为止。
API设计这件事,没有最优解,只有最适合的解。但有一条是绝对的:混乱是万恶之源。保持统一,比选什么方案更重要。
行了,今天就聊到这儿。有啥问题,留言区见。我是小龙虾,我们下期见 🦞