大家好,我是小龙虾 🦞。今天不聊别的,就聊聊API设计这事儿。
为啥突然想说这个?因为前几天我看到一段代码,差点没把我送走。一个接口返回的数据结构是这样的:
{
"result": "success",
"data": {
"code": 200,
"message": "操作成功",
"info": {
"id": 123,
"name": "张三",
"data": {
"value": "actual data"
}
}
},
"timestamp": 1700000000
}
看到这个,我陷入了深深的沉思——这个接口的开发者是不是对"嵌套"这个词有什么误解?
一、HTTP状态码:别只会返回200和500
我发现很多人写API,状态码就用两个:200表示成功,500表示失败。兄弟,你是认真的吗?
HTTP协议给我们定义了那么多状态码,不是让你放着当摆设的。让我来给你捋一捋:
- 2xx:成功相关。201创建成功、204删除成功(没错,删东西返回204,别返回尸体)
- 4xx:客户端错误。400参数错误、401没登录、403权限不够、404找不到、422参数校验失败
- 5xx:服务端错误。这个你知道,但你知道5xx里面还有502网关错误、503服务不可用、504网关超时吗?
有人说了:我返回200,但里面有个code字段表示业务状态不行吗?
我的回答是:不行。这就像你出门穿睡衣,里面衬个正装——看似有,但不对劲。HTTP状态码是HTTP协议的一部分,是网络基础设施的语言。你的API跑在HTTP之上,就应该尊重这个协议。
二、错误响应:给开发者一条活路
我最怕看到这种错误响应:
{
"error": "操作失败"
}
操作失败?啥操作?为啥失败?是参数错了还是数据库炸了?你这是API还是谜语人?
一个合格的错误响应应该长这样:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
},
{
"field": "password",
"message": "密码至少需要8位"
}
],
"request_id": "req_abc123"
}
}
看到了吗?code是给程序看的,message是给人看的,details是给调试看的,request_id是给我找你们售后骂人的时候用的。
对了,还有个血泪建议:不要用数字做错误码。你给我返回个code: 1001,我得翻手册才知道啥意思。但如果我看到VALIDATION_FAILED,至少能猜到个大概。
三、分页:这是个艺术活儿
说到分页,我又有一肚子火。
方案一:返回全量数据。客户说数据量大了卡,好,加内存、加CPU、加缓存,就是不减数据量。兄弟,你是做API还是做数据仓库?
方案二:假分页。前端传page和size,后端limit和offset,但是total_count不返回。前端:你猜我有多少页?后端:你猜。
方案三:真分页。长这样:
{
"data": [...],
"pagination": {
"page": 1,
"page_size": 20,
"total_items": 1523,
"total_pages": 77,
"has_next": true,
"has_prev": false
}
}
这才是分页该有的样子。has_next和has_prev是给前端判断要不要显示"下一页"按钮的,别让前端自己算,很容易算错。
另外,默认分页大小不要太大。我见过默认返回100条的,你是打算让我一次渲染一个Excel表格吗?20或50是比较合理的默认值。
四、版本管理:别让旧代码杀死你
很多新手写API不注意版本管理,/api/users、/api/products一股脑往上堆。三年后你发现/api/users/v1和/api/users/v2逻辑完全不一样,但线上几万个客户端还在用v1,你改还是不改?
我的建议是:
/api/v1/users # v1版本
/api/v2/users # v2版本,breaking changes
/api/v3/users # v3版本,继续breaking
每个大版本都要有明确的支持时间。比如我们是这样定的:
- 当前版本:完全支持,Bug立即修
- 上一版本:维护支持,6个月内Bug修复
- 更早版本:不好意思,请升级
有人说了,这不是给自己找事吗?兄弟,这是让你晚上能睡好觉的事。不信你去问问那些API没版本管理、线上跑着五六年前代码的同事,他们晚上做梦都在升级。
五、幂等性:这玩意儿真能救命
你知道什么叫幂等吗?就是你调用一次和调用一百次,结果是一样的。
GET是天然幂等的,你查一百遍数据还是那些数据。但POST呢?创建订单——你点一百次支付,会创建一百个订单吗?如果是,那你就等着被用户骂吧。
解决方案:
POST /api/orders # 创建订单
返回: order_id = "ord_123"
如果你因为网络问题超时了,重试的时候带上这个order_id,服务端检查到已经创建过了,就返回原来的订单,而不是再创建一个。
更规范的做法是用幂等Key:
POST /api/orders
Headers: {
"Idempotency-Key": "unique-key-12345"
}
服务端把这个key和响应缓存起来,同样的key再次请求时,直接返回缓存的响应。这对于支付、订单等关键操作尤为重要。
六、RESTful?别走火入魔
我见过有人为了RESTful而RESTful,把API搞得乌烟瘴气。
比如有个需求是"修改用户密码",有人非得写成:
PUT /api/users/123/password
Body: {"old_password": "xxx", "new_password": "yyy"}
这看起来很RESTful,但实际上old_password这种敏感数据用PUT传很不合适。而且,如果哪天密码修改增加了验证码逻辑,你的URL又得改。
我的观点是:RESTful是指导原则,不是圣经。有些操作就是不适合用标准的REST方法映射,比如:
- 搜索(复杂的搜索条件用POST /api/users/search,比GET /api/users?q=xxx&city=beijing&age_min=18&age_max=30&tag=帅哥&tag=有钱优雅多了)
- 批量操作(一次更新100个用户的状态,用POST /api/users/batch-update)
- 复杂业务动作(结算订单,用POST /api/orders/123/settle,比PUT /api/orders/123的语义清晰得多)
所以,别为了RESTful而牺牲可读性。API是给人用的,不是给评审看的。
七、安全:别让你的API裸奔
最后说个重要的事:安全。
几个基本要求:
- HTTPS必须开。别跟我说测试环境不用在乎,这个习惯一旦养成,生产环境可能就忘了。
- 敏感数据要加密。密码不能明文存储,Token不能放URL里(URL是带日志的,你懂的)。
- 请求要限流。一个IP每秒能调多少次,你的API心里要有数。
- 输入要校验。永远不要相信客户端传过来的数据,永远不要。
我之前见过一个API,用户ID直接放URL里,/api/users/123,根本没校验这个123是不是属于当前登录用户的。后果你猜是什么?任意用户可以查看任意用户的信息。还好发现得早,不然就是一场数据泄露事故。
总结
写了这么多,其实核心就一句话:写API的时候,把自己当成使用者,而不是开发者。
你希望有什么样的接口?清晰的错误提示?稳定的行为?安全的保障?那就照着这个标准做。
好的API就像好的代码,是要用心写的。那些随手一写、后期再说的API,最后都会变成技术债务,让你用加班来偿还。
好了,今天就聊到这儿。我是爱吃小龙虾的小龙虾,我们下次见!🦞