干过后端开发的都知道,写业务代码其实不算难,真正的难点在于接口设计。一个烂接口可以让你后续维护时恨不得穿越回去把写它的人打一顿——然后发现自己就是那个人。
今天来聊聊我这些年踩过的坑,以及怎么绕过去。纯属个人血泪史,拿出来给大家当反面教材。
1. HTTP方法乱用,你以为POST和PUT真的没区别?
先问个问题:更新用户信息,用POST还是PUT?
很多人会说"随便,反正功能一样"。错,大错特错。我见过有人全站更新操作全用POST,问就是"习惯了"。
正确姿势:
GET /users/123 # 查看用户
POST /users # 创建用户
PUT /users/123 # 完整更新用户(幂等)
PATCH /users/123 # 部分更新用户(非幂等但可重试)
DELETE /users/123 # 删除用户
幂等性是个很重要的概念。PUT是幂等的,调用一次和调用十次结果一样;POST不是幂等的。你要是把订单支付这种操作用PUT,等着被投诉吧——用户疯狂点支付,接口给你重复扣款十次,哭都来不及。
还有个经典的:搜索用GET还是POST?答案是GET。URL参数、Query String,天经地义。POST适合那种请求体比较复杂的场景,比如上传文件、发送大JSON。搞反了的话,浏览器后退按钮都能给你整出bug来。
2. 状态码乱返回,200表示"我成功了"?太天真
有人返回200,但code字段写着400,问我为什么前端一直报接口失败。我:???
标准HTTP状态码是给谁看的?是给HTTP层看的,是给CDN、网关、反向代理看的。你在里面塞个业务层的错误码,前面的基础设施全傻了,不知道你这接口到底成没成功。
最常用状态码记住这张表就够了:
2xx - 成功
200 OK # 标准成功
201 Created # 创建资源成功
204 No Content # 成功但没返回内容(比如DELETE)
4xx - 客户端错误
400 Bad Request # 请求参数有问题
401 Unauthorized # 没登录
403 Forbidden # 没权限
404 Not Found # 资源不存在
409 Conflict # 冲突(比如重复创建)
422 Unprocessable Entity # 参数格式对但语义错
429 Too Many Requests # 请求太快了,歇会儿
5xx - 服务器错误
500 Internal Server Error # 代码bug
502 Bad Gateway # 依赖服务挂了
503 Service Unavailable # 服务不可用
504 Gateway Timeout # 超时
特别想说一下401和403的区别。很多人混用,但实际上:401是"你还没认证,我需要你证明身份";403是"你身份验证过了,但你没权限干这事"。搞反了前端登录逻辑会乱成一锅粥。
还有个新手常犯的:把所有错误都返回200,然后在body里写{code: 500, message: "服务器错误"}。你这是欺负HTTP层的中间件都不识字吗?网关看到200就认为成功了,然后你的"错误"就这么静默通过了。
3. 分页设计反人类,第0页是什么鬼?
很多API的分页参数是这样的:
GET /users?page=0&size=20
程序员是从0开始数数的,但产品经理、测试、甲方爸爸不是。"你给我看第0页?数据呢?"然后你得解释半天为什么从0开始。
更科学的方案是用游标分页(Cursor Pagination):
GET /users?cursor=eyJpZCI6MTIzfQ&limit=20
返回:
{
"data": [...],
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true
}
游标分页的好处:
- 不管数据怎么变化,新增数据不会导致重复或遗漏
- 前端不用算页码,"有没有下一页"一目了然
- 性能稳定,不随"页数"增大而变慢
如果非要用工号分页,用偏移量分页时建议用offset而不是page,至少语义清晰:
GET /users?offset=40&limit=20 # 从第40条开始,取20条
4. 路径设计随心所欲,RESTful是什么可以吃吗?
见过最离谱的接口:
POST /getUserInfo
POST /user/get_info
POST /queryUserById
POST /fetchUserData
同一个功能,四种写法。这要是接手别人的代码,能把人逼出高血压。
RESTful规范其实没那么复杂:
# 资源用名词,不是动词
GET /articles # 文章列表
GET /articles/123 # 单篇文章
POST /articles # 创建文章
PUT /articles/123 # 更新文章
DELETE /articles/123 # 删除文章
# 嵌套资源表达关系
GET /users/123/articles # 这个用户的所有文章
GET /users/123/articles/456 # 具体某篇
# 查询参数做过滤/排序/分页
GET /articles?status=published&sort=created_at&order=desc
有人问:批量操作怎么办?比如同时删除100个用户。标准REST没有批量操作的规范,但业界通用的做法是:
POST /users/batch
Body: { "ids": [1, 2, 3, ..., 100], "action": "delete" }
或者用一种更"REST"的方式:
POST /users/batch-delete
Body: { "user_ids": [1, 2, 3...] }
别纠结,实用第一。
5. 错误信息形同虚设,"操作失败"能再敷衍点吗?
这个星球上最让人崩溃的报错是什么?
{
"code": 400,
"message": "操作失败"
}
操作失败是什么鬼?为什么失败?是我传参错了还是服务器炸了?用户看到这种错误,99%会疯狂重试,然后你的系统压力翻倍,然后真·服务器炸了。
好的错误响应应该长这样:
{
"code": 422,
"message": "参数校验失败",
"errors": [
{
"field": "email",
"message": "邮箱格式不正确",
"value": "this is not email"
},
{
"field": "age",
"message": "年龄必须大于0",
"value": -1
}
],
"request_id": "req_abc123" # 方便排查的请求ID
}
几个关键点:
- 明确指出是哪个字段出错,前端可以精准定位到输入框
- 说清楚错误原因,不是"非法"而是"邮箱格式不正确"
- 带上出错的具体值,方便复现问题
- 加上request_id,出问题时日志搜索一把梭
还有个大坑:生产环境的错误信息不要包含敏感信息(比如SQL语句、内部文件路径、堆栈信息),但测试/预发环境可以详细些——方便开发调试。区分环境,这点很重要。
6. 版本管理裸奔,v1/v2/v3到底是什么?
API要升级,但老接口不能动,怎么办?版本化。
常见的版本化方式:
# URL版本(GitHub、Stripe在用)
GET /v1/users
GET /v2/users
# Header版本(更RESTful,但不够直观)
GET /users
Accept: application/vnd.myapp.v2+json
# Query参数(简单但不够规范)
GET /users?version=2
我的建议:用URL版本。原因很简单——调试方便。curl直接敲,不用加header,浏览器直接输地址,不用改Accept头。 Stripe的API文档满天飞,但凡用过Stripe的人都夸它文档好,其中一个原因就是版本在URL里,一目了然。
版本发布节奏建议:
- 新版本发布后,老版本至少保留6-12个月
- 每个版本有明确的EOL(end of life)日期
- 重大Breaking Change必须升版本
- 小功能迭代可以向后兼容,就别升版本折腾人了
7. 忽略API安全,基本裸奔就上线
这条是重中之重,放最后压轴说。
我见过太多接口:没有认证、没有限流、没有参数校验、没有防注入——就那么赤条条地上线了。然后:
- 被爬虫一夜爬光所有数据
- 被恶意用户高频请求打挂
- 被SQL注入把数据库删了个干净
- 被XSS攻击搞了个弹窗满天飞
必做的几件事:
① 认证和授权:JWT token、OAuth2、Session,随你选但必须有。没有认证的接口就像没锁的门,谁都能进。
② 限流(Rate Limiting):防止刷接口、防止DDoS。常用算法:固定窗口、滑动窗口、令牌桶、漏桶。简单场景用固定窗口就行:
# 响应头告诉前端还剩多少额度
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000 # 窗口重置时间戳
超限了返回429 Too Many Requests,别返回200然后在body里说"请求太频繁"——中间件看不懂。
③ 参数校验:所有输入都是有毒的。后端必须校验数据类型、长度、格式、范围。别相信前端传过来的任何东西。
④ 防注入:SQL注入、NoSQL注入、XSS、CSRF,能防的都防。用ORM框架的参数化查询,别自己拼SQL字符串。
写在最后
API设计这事,说难不难,说简单也不简单。核心就一句话:设计时多想一步,实现时少踩一坑。
接口是给程序员用的,但更是给未来接手的人用的,也是给自己老了之后不想加班返工用的。写的时候多花10分钟设计,后面的维护成本可能省下的是10小时。
当然,最重要的还是——写完接口记得自测。一个自己都不愿意用的接口,就别指望测试会放过你了。
大家还有什么接口设计上的血泪史,欢迎评论区分享,我先去哭一会儿。