写了5年代码,我发现API设计才是程序员的分水岭
干这行这么多年,我见过太多「能用就行」的API。有些系统代码写得稀烂,但API设计得优雅,后期维护起来像散步;有些同学代码写得飞起,API却一塌糊涂,对接方骂骂咧咧,后面接手的人恨不得顺着网线过来打人。
API设计这事,说大不大,说小不小。但你一旦踩过那些坑,就会明白:一个烂API可以让一个团队痛苦三年。今天咱们就聊聊那些最常见的API设计错误,以及怎么避坑。
一、URL里塞动词,REST看了会流泪
先问个问题:下面这两个API,哪个更「REST」?
GET /getUser?id=10086
GET /users/10086
对,第二个。这就是REST的核心思想之一:用名词,不用动词。资源是名词,动作交给HTTP方法。
但我见过更离谱的:
POST /deleteUser
POST /updateUserInfo
GET /queryAllOrders
哥们,POST是POST,DELETE是DELETE,你搁这套娃呢?这种设计不叫REST,叫「假装REST」。时间长了,自己都忘了哪个接口是干啥的。
正确姿势:
DELETE /users/10086 # 删除用户
PATCH /users/10086 # 部分更新用户
GET /users/10086 # 获取用户
GET /orders?user_id=10086 # 查某用户的订单
PATCH很多人不用,但它就是给「只改用户头像」这种场景准备的。记住,PUT是全量替换,PATCH是部分更新,别用反了。
二、状态码乱用,前端同学半夜报警
HTTP状态码是API的语言。你跟前端说「200」,他就知道「成了」;你说「500」,他就知道「服务器炸了,你等着」。但如果你乱用状态码,前端会陷入无尽的困惑。
最常见的问题:所有错误都返回200,然后在body里塞个code: 500。
// 这个API实际上出错了,但前端看到200以为一切正常
HTTP/1.1 200 OK
{"code": 500, "message": "数据库连接失败"}
拜托,HTTP状态码就是用来干这个的!你这样搞,前端监控报警都配置不好,因为他看到200就认为没问题。
标准状态码清单(按场景用):
- 200 — 成功(GET、PATCH、PUT成功)
- 201 — 资源创建成功(POST成功)
- 204 — 成功但无返回内容(DELETE成功)
- 400 — 前端传参有问题,比如格式不对、缺字段
- 401 — 未认证,没登录或Token过期
- 403 — 已认证但没权限
- 404 — 资源不存在
- 409 — 冲突,比如重复创建
- 422 — 参数校验失败
- 500 — 服务器自己出问题了
别偷懒用200+code的方式,状态码是给整个HTTP生态看的,CDN、网关、监控都靠它判断。
三、分页随心所欲,每个接口都不一样
有的接口这样分页:
GET /users?page=1&page_size=20
另一个接口这样:
GET /orders?offset=0&limit=20
还有一个这样:
GET /products?start=0&count=20
前端同学:???你们后端是不是有KPI,接口数量越多越好?
统一分页方式,不仅是风格问题,更是契约。前端封装SDK、接口文档、联调测试,全都依赖这套约定。
推荐方案,Cursor分页(游标分页)vs 偏移分页都要支持:
# 偏移分页,适合数据量可控的场景
GET /orders?page=2&page_size=20
# 响应
{
"data": [...],
"pagination": {
"page": 2,
"page_size": 20,
"total": 300,
"total_pages": 15
}
}
如果数据量大、增长快,用游标分页避免深分页性能问题:
GET /orders?cursor=eyJpZCI6MTAwMH0&limit=20
# 返回
{
"data": [...],
"next_cursor": "eyJpZCI6MTAyMH0",
"has_more": true
}
四、错误信息等于没信息
最让人崩溃的API错误是这样:
{"error": "操作失败"}
操作失败?什么操作?为啥失败?是我的问题还是服务器的问题?前端同学看着这个错误,只能给你回一句「接口报错了」。
好的错误响应应该长这样:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
},
{
"field": "age",
"message": "年龄必须在18-120之间"
}
],
"request_id": "req_abc123xyz"
}
}
code是给程序看的,方便前端做分支处理;message是给用户看的,可以直接展示也可以做国际化;details列出具体哪个字段出了什么问题;request_id是排查问题的钥匙,出了事让用户报这个ID,一查就知道是哪次请求。
很多团队不屑于做这些,觉得麻烦。但你想想,每次联调省下的十分钟,最后都会在生产环境以小时为单位还回来。
五、版本管理形同虚设
「没事,我们接口稳定,不用做版本管理。」
说这话的团队,一般在三个月后会有一堆/api/v1、/api/v2、/api/v3、/api/v4在代码库里并存,然后没人知道哪些接口在用、哪些已经废弃了。
API版本不是过度设计,是给自己留后路。当你要改一个字段名、加一个必填参数、改一个返回结构时,没有版本隔离,你就是在谋杀下游。
GET /api/v1/users/10086
GET /api/v2/users/10086 # v2改了返回结构,加了 avatar 字段
版本放URL里是最直观的方式,也可以放Header里(Accept: application/vnd.api+json; version=2),但URL方式更容易做网关路由和文档,也方便前端调试。
记得,发布新版本时,老版本至少再维护6个月再废弃。别学某些大厂,说停就停,恨不得让全球开发者第二天就跟着改。
六、不做幂等性设计,交付那天就是噩梦
什么是幂等?就是你调用一次和调用一百次,效果一样。比如删除操作,删第一次删掉了,删第二次还是删掉了(没东西可删,但也没报错),这叫幂等。但如果是这样的接口:
POST /orders/10086/pay # 扣款接口
POST /refund # 退款接口
这两个操作如果没做幂等控制,用户网络抖动点了两下,钱就扣了两次,或者退了两笔。这种问题在支付场景是致命的。
解决方案:用幂等Key。客户端生成一个全局唯一ID,放进Header,服务器根据这个ID做去重:
POST /orders/10086/pay
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
# 服务器先查这个key是否处理过:
# - 没处理过:执行业务逻辑,返回结果
# - 处理过:直接返回上次的结果,不重复执行
金融类的接口,幂等是基本素养,不是可选项。
写在最后
API设计不是什么高大上的架构设计,它就是天天要打的交道。但正因为天天见,那些细节才重要。一个好的API,用起来像呼吸一样自然;一个烂API,用起来像便秘。
代码可以重构,API一旦对外发布,动起来就难了。所以在一开始就想清楚,花20%的时间设计,省下往后200%的时间填坑。
如果你现在手头有个「能用就行」的API,建议抽个空 review 一下。不用一次性改完,先从最影响使用的地方开始。慢慢来,别指望一口吃成胖子。
毕竟,程序员最远的路,就是「下次一定重构」。