API设计翻车实录:那些年我们一起踩过的坑
干后端开发的,谁没在API设计上翻过车呢?我见过最离谱的一个接口,返回一个用户信息居然要调三次接口——第一次拿基本信息,第二次拿扩展信息,第三次拿所谓的「安全信息」。客户端同学调接口调到怀疑人生,最后直接在前端写了三个请求并行发,代码看起来像是在召唤神龙。
今天不整虚的,来聊聊API设计中那些让人血压飙升的坑,以及怎么优雅地绕过去。
一、RESTful不是银弹,别为了形式丢了实用性
2010年代那会儿,RESTful API几乎成了「专业」的代名词。所有人都说要用名词不用动词,要用HTTP方法语义,结果呢?
一个订单系统里,你需要:
GET /orders # 查订单列表
POST /orders # 创建订单
GET /orders/{id} # 查单个订单
PUT /orders/{id} # 更新订单
DELETE /orders/{id} # 删除订单
PATCH /orders/{id}/cancel # 取消订单...等等,这还是RESTful吗?
到了「取消订单」这个操作,RESTful的纯粹性就开始碎裂了。有人坚持用PATCH把整个订单状态位改掉,有人偷偷加了/action/Cancel这样的动词尾巴,有人干脆开了个/webhooks/resolver这种听起来就很敷衍的接口。
我的观点是:RESTful是个好思想,但别让它绑架了你的业务。有些操作天然就是动宾结构,强扭的瓜不甜,强行RESTful只会让接口看起来像是在写八股文。
实战建议:简单场景用REST,复杂业务逻辑用RPC风格也完全没问题。GraphQL和gRPC也是工具,不是信仰。
二、分页不是简单limit offset,能不用就别用
最常见的分页实现:
GET /users?page=1&page_size=20
背后的SQL大概是这样:
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 20;
数据少的时候没问题,数据多了试试?offset到十万级别的时候,数据库得先把前十万行给你「扔掉」,这个扔掉的动作可是实打实要扫描的。
更好的方案是什么?游标分页(Cursor-based Pagination)。不再说「给我第5页」,而是说「给我id大于1000之后的20条」。
GET /users?after=1000&limit=20
# 返回 users 数组 + next_cursor 字段
SQL变成:
SELECT * FROM users WHERE id > 1000 ORDER BY id LIMIT 20;
不管数据有多少万条,这个查询永远是索引范围内的高效扫描,时间复杂度O(log n)稳定输出。朋友圈、微博、抖音的feed流,全都是游标分页,你以为他们用的是page参数?天真了。
三、错误处理:你在返回200但其实已经错了
这是最让人无语的反模式:
HTTP/1.1 200 OK
{
"success": false,
"error": {
"code": 10001,
"message": "余额不足"
},
"data": null
}
HTTP状态码是200,但业务上已经报错了。客户端得先看你success字段才知道成没成功,这不是脱了裤子放屁吗?
正确做法:
HTTP/1.1 402 Payment Required
{
"error": {
"code": 10001,
"message": "余额不足"
}
}
把HTTP状态码用起来,这是协议层给我们的资产:
- 400:客户端问题(参数错误、格式不对)
- 401:未认证
- 403:已认证但没权限
- 404:资源不存在
- 422:语义正确但业务不允许(比如余额不足这种「逻辑错误」)
- 429:请求过于频繁
- 500:服务端故障
有人会说422不太常见,浏览器不太认识。但你的API是给程序调用的,不是给浏览器看的,程序在乎的是语义清晰,不是浏览器认不认识这个状态码。
四、版本管理:URL版本号才是yyds
API版本管理的策略主要有三种:
1. URL路径版本(最常见):GET /api/v1/users
2. Header版本:API-Version: 2023-01-01
3. 查询参数版本:GET /api/users?version=1
我见过用Header版本的团队,调试接口的时候得用Postman手动加Header,打日志的时候得专门记录是哪个版本。curl测试还得记得每次都加-H "API-Version: xxx",多了以后测试同学恨不得把电脑砸了。
URL版本号的最大优势是什么?可见性。日志里一清二楚哪个版本的接口被调了,缓存粒度可以精细到版本级别,API文档里不同版本的接口泾渭分明。
至于URL里带版本号「不RESTful」这种说法——前面说过了,别让形式大于内容。
五、批量接口:你真的需要N+1吗?
假设前端要展示一个仪表盘,需要用户信息、订单统计、待处理任务三个数据块。
一个没经验的实现:
GET /current_user
GET /orders/stats
GET /tasks/pending
三个请求串行,假设每个200ms,用户得等600ms才能看到完整页面。如果搞成并行,虽然快了一倍,但三个请求各自走一遍网络开销和连接建立时间,总耗时还是在300ms以上。
更好的设计:
GET /dashboard
# 返回:
{
"user": {...},
"order_stats": {...},
"pending_tasks": [...],
"generated_at": "timestamp"
}
一次请求,后端做数据聚合,前端拿到就是完整的。而且后端可以对这些数据做本地缓存,数据库查询也能复用同一条连接,减少连接建立的开销。
写在最后
API设计这件事,说到底是给调用方的一份承诺。你设计的每一个接口,都在定义别人使用你系统的方式。接口烂,调用方就痛苦;接口好,调用方写代码就是享受。
别追求花里胡哨的概念,把握住几个核心:语义清晰、分页合理、状态码正确、版本管理清晰、少做N+1。做到这几点,你的API至少不会让人想砸键盘。
当然,最重要的还是——写接口之前先问问自己:如果我是调用方,我会想怎么用?有时候换位思考一下,比看十篇架构论文都有用。