干后端开发这些年,我见过太多让人血压飙升的API设计。有的接口返回嵌套八层JSON,有的接口GET请求带body,还有的接口用200状态码返回错误。这些问题,你或多或少都见过。今天咱们就来聊聊那些年我们一起踩过的API设计坑,以及怎么优雅地爬出来。
一、RESTful不是考试,别为了"规范"把自己考死
很多人第一次接触RESTful规范的时候,就像刚学做饭的新手,非要严格按照菜谱来,结果做出来的菜能吃但总觉得哪里不对劲。
比如,有个兄弟要给商品打标签,RESTful最佳实践告诉他要用POST /products/{id}/tags。他确实这么做了。结果产品经理说:我们还需要批量给多个商品打标签。他说不好意思,RESTful规范不支持这个操作,建议你一个一个打。
这就是教条主义的典型症状。RESTful是指导原则,不是法律条文。它的核心是:用统一的接口暴露资源,让客户端可以通过标准方法操作资源。在这个基础上,灵活一点不丢人。
真正的RESTful高手,是那些知道什么时候该遵守规则,什么时候该打破规则的人。
我现在的做法是:如果你的业务场景用标准HTTP方法和资源模型能自然表达,那就用;如果不能,加个action或者自定义方法名不丢人。/bats/batch-add-tags 怎么了?清晰明了,比硬凹姿势强一百倍。
二、HTTP状态码:别只会200和500
这个问题我面试的时候必问:HTTP状态码分几类?很多人能背出来1xx、2xx、3xx、4xx、5xx,然后就没有然后了。
实际工作中呢?几乎所有接口都是200 OK,错误信息全塞在response的code字段里。我见过最离谱的是一个接口,登录失败返回200,body里写着{"code": 401, "message": "密码错误"}。我第一次看到的时候还以为见鬼了——200还能登录失败?
拜托,HTTP状态码是给谁用的?是给HTTP库、API网关、CDN、浏览器这些基础设施用的。你返回200表示一切正常,它们就会正常缓存、正常展示。只有返回4xx或5xx,它们才知道出了问题。
所以,请正常使用状态码:
- 400 Bad Request:请求参数有问题,客户端自己检查去
- 401 Unauthorized:没登录或token过期,去登录
- 403 Forbidden:登录了但没权限,别试了
- 404 Not Found:资源不存在
- 429 Too Many Requests:请求太快了,慢点
- 500 Internal Server Error:服务端出问题了
当然,也不是说要把所有状态码都用一遍。我的经验是:优先使用标准状态码表达语义,错误信息放在body里补充说明。这样基础设施和人类都能看懂。
三、错误响应:把你的错误信息当产品来设计
很多后端工程师写错误响应是这样的:
{
"code": 1001,
"message": "操作失败"
}
操作失败?什么操作?哪里失败了?为什么失败?客户端拿到这个能干什么?答案是:什么都干不了,只能展示一个"操作失败"给用户,然后用户一脸懵逼。
好的错误响应应该像这样:
{
"error": {
"code": "INVALID_PARAMETER",
"message": "请求参数不合法",
"details": [
{
"field": "email",
"message": "邮箱格式不正确",
"value": "not-an-email"
}
],
"request_id": "req_abc123xyz",
"help_url": "https://api.example.com/docs/errors#INVALID_PARAMETER"
}
}
这个错误信息告诉客户端:哪里错了(email字段)、为什么错(格式不正确)、错的值是什么(not-an-email)、怎么解决(看文档)、出了问题找谁(request_id)。这才叫有价值的错误信息。
我建议每个团队都维护一份错误码字典,就像维护一份产品功能文档一样。错误码要有清晰的分类:1开头是参数错误,2开头是认证错误,3开头是权限错误,4开头是业务逻辑错误,5开头是服务端错误。每个错误码都要有文档说明:什么情况下会发生、客户端应该怎么响应、需不需要重试。
四、版本管理:别让你的接口变来变去
有一种痛苦叫做:接口上线三个月,产品经理说"这个字段能不能改个名字"。你改了,然后线上几十个调用方全部爆炸。
所以,API设计一定要有版本概念。不是说我会在接口里加个版本号那么简单,而是要把版本当作契约来对待。
常见的版本策略有三种:
1. URL版本:/api/v1/users、/api/v2/users
优点:直观、强制、便于路由
缺点:版本粒度太粗,改一个字段要升级整个API
2. Header版本:Accept: application/vnd.example.v2+json
优点:URL干净,版本粒度可控
缺点:不够直观,测试和调试麻烦
3. Query参数版本:/api/users?version=2
优点:灵活,客户端可以选择
缺点:容易被忽略,不利于基础设施处理
我的建议是:URL版本是最务实的选择。你可以在nginx里直接路由到不同版本,可以快速定位问题,可以明确告诉调用方用哪个版本。除非你有非常特殊的理由,否则别玩花活。
另外,每个版本都要有明确的生命周期:什么时候推出、什么时候弃用、什么时候下线。给调用方足够的时间迁移,但也要有底线,不能永远支持旧版本。
五、分页:这是门艺术,不是体力活
"给我查所有用户"——这是产品经理最常说的话,也是后端工程师最怕听到的话。因为他们知道,这句话后面跟着的一定是一个没有分页的接口,然后数据量大了之后数据库直接升天。
分页看起来简单,但其实有很多坑:
1. 偏移分页 vs 游标分页
偏移分页(offset-based)是最常见的:
GET /users?page=1\&page_size=20
简单是简单,但有个致命问题:数据有变化的时候会出现错位。比如你正在翻页的时候,有人删了一个用户,你下一页就可能跳过一个用户,或者看到一个重复的用户。在数据频繁变化的场景下,这简直是噩梦。
游标分页(cursor-based)就不一样了:
GET /users?cursor=eyJpZCI6MTAwfQ\&page_size=20
游标是上一页最后一条记录的标识,服务端根据游标定位,不依赖偏移量,所以不会受数据变化影响。缺点是:不能随机跳页,只能一页一页翻。
什么时候用哪种?
- 数据相对稳定、不需要随机跳页:用游标分页
- 需要支持跳页、跳到指定页:用偏移分页(但要做好性能优化,比如用覆盖索引)
2. 分页响应格式
这个问题很多人不重视,结果客户端拿到分页数据还要自己算。分页响应应该包含:
{
"data": [...],
"pagination": {
"total": 1000,
"page": 1,
"page_size": 20,
"total_pages": 50,
"has_next": true,
"has_prev": false
}
}
这样客户端拿到就能直接展示,不用再算总数、算页数、算有没有下一页。
六、写在最后
API设计这事,说难听点,是后端工程师的脸面。你写的接口好不好用、规不规范、有没有坑,别人一用就知道。
但说到底,API设计没有绝对的标准,只有最适合的选择。规范是死的,人是活的。理解规范背后的原理,然后根据实际场景做决策,这才是正确的态度。
下次设计接口之前,多问自己几个问题:这个接口的使用者是谁?会有什么样的调用场景?出错了怎么排查?数据量大了怎么办?
想清楚这些,你的接口就不会太差。
至于那些还在用200返回错误、用字符串存JSON、用拼音写字段名的兄弟……祝你平安。