做后端开发这么多年,我见过最离谱的事情就是:一个团队五个人,写出了七种不同的API风格。有人用POST做一切,有人把DELETE当GET用,还有人返回值里塞emoji表示状态码。👌
一、URL不是文件系统
见过最离谱的API是这样的:
GET /api/getUserById?id=123
GET /api/getAllUsers
POST /api/createUser
POST /api/updateUser
POST /api/deleteUser
兄弟,你这是在写SQL还是在写API?RESTful不是让你把SQL关键字翻译成英文塞进URL里。
正确的做法是什么?
GET /users/123 # 获取单个用户
GET /users # 获取用户列表
POST /users # 创建用户
PUT /users/123 # 更新用户(完整更新)
PATCH /users/123 # 部分更新
DELETE /users/123 # 删除用户
记住:URL是资源,不是动作。名词复数形式是标准做法。别再用getUserById这种写法了,看着让人血压升高。
二、HTTP状态码:别再什么都返回200了
很多人的API无论成功失败都返回200,然后在前端代码里判断返回值里有没有error字段。这不是不行,但这是在给自己挖坑。
HTTP状态码是干嘛用的?是让调用方一眼就知道请求结果用的。以下是真正有用的状态码指南:
- 200 OK - 请求成功,别滥用
- 201 Created - 资源创建成功,记得返回创建后的完整对象
- 204 No Content - 删除操作成功,空响应体就够了
- 400 Bad Request - 参数校验失败,这时候要把具体错误信息返回给前端
- 401 Unauthorized - 没登录,别跟我装
- 403 Forbidden - 登录了但没权限
- 404 Not Found - 资源不存在
- 422 Unprocessable Entity - 语义错误,比如邮箱格式对但收件人不存在
- 429 Too Many Requests - 限流了,给前端留个Retry-After
- 500 Internal Server Error - 服务器挂了,这个错误信息不要返回给用户
最搞笑的是有些API成功返回200,失败也返回200,全靠前端解析。这种API我称之为薛定谔的API——只有调用了才知道成功还是失败。
三、分页:你的接口为什么这么慢?
假设你有100万用户,要返回一个列表给前端。你会怎么做?
方案A:一次性全量返回,反正前端会做分页。
恭喜你,中型企业级事故就这么发生了。前端加载一个页面要30秒,用户骂产品经理,产品经理骂后端,后端骂数据库,数据库说我也很难啊。
方案B:服务端分页
GET /users?page=1&page_size=20
这才是正确的打开方式。但光分页还不够,还得注意:
返回结果里要有总数和分页信息:
{
"data": [...],
"pagination": {
"total": 1000000,
"page": 1,
"page_size": 20,
"total_pages": 50000
}
}
为什么要total_pages?因为前端要渲染分页器,不知道总页数怎么渲染?靠前端自己算?万一前端用的是PHP呢(开玩笑,PHP也能算)。
四、版本管理:你的API能向前兼容吗?
想象一下这个场景:你上线了v1版本的API,三个月后产品说要改字段名。你发现线上已经有三十多个系统在调用这个接口,改也不是,不改也不是。
所以API版本管理要从第一天就设计好。常见方案:
方案一:URL版本(最常用)
GET /api/v1/users
GET /api/v2/users
方案二:Header版本
GET /api/users
Accept: application/vnd.myapi.v2+json
方案三:Query参数(不推荐)
GET /api/users?version=2
我的建议是用方案一,简单粗暴,前端好调试,nginx配置也方便。方案二看着优雅,但每次调试都要改header,实际开发中特别烦人。
五、错误响应:给前端留条活路
错误响应是API设计中最重要的部分之一,却也是最容易被忽略的。一个好的错误响应应该是这样的:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
},
{
"field": "password",
"message": "密码长度必须大于8位"
}
]
}
}
这样前端可以直接根据code做逻辑处理,根据details渲染具体的错误提示。用户看到的是「密码长度必须大于8位」,而不是一堆不知所云的JSON。
反面教材:
{
"error": true,
"msg": "操作失败",
"info": "请联系管理员"
}
这种响应让我怀疑写这个API的人是不是跟前端有仇。
六、幂等性:你确定这个操作可以重试吗?
网络是不稳定的。请求发出去一半断了,前端问你:要不要重试?
如果你说能重试,那这个接口必须是幂等的。简单来说就是:同一个请求执行一次和执行多次,效果是一样的。
# 幂等操作
PUT /users/123 # 更新操作,幂等
DELETE /users/123 # 删除操作,幂等
POST /users/123/retry # 明确的重试接口,幂等
# 非幂等操作
POST /orders # 创建订单,每次调用创建新订单
POST /payments # 扣款,每次调用扣一次钱
对于非幂等操作,怎么处理?可以用唯一请求ID:
POST /api/v1/orders
X-Request-Id: uuid-xxxx-xxxx
# 服务器记录这个ID,如果重复提交,返回之前的创建结果而不是创建新订单
这样既保证了接口的幂等性,又不用强制前端做重试判断。
写在最后
API设计看起来是技术活,实际上是产品和技术的桥梁。你的API好不好用,直接决定了前端同学会不会在群里喷你。
记住几个原则:
- URL是资源,不是动作
- 状态码是给人看的,不是摆设
- 分页要从第一天就做,别等数据量破百万再想起来
- 版本管理要提前规划,别等出事了再打补丁
- 错误响应要详细,但不要暴露内部细节
- 幂等性是网络不稳定时代的必备技能
好的API设计不会让你出名,但差的API设计一定会让你背锅。共勉。