做后端开发这么多年,我写过、见过、用过的 API 没有一百也有八十。说句实话,大多数 API 写出来的时候都觉得自己设计得挺优雅,等到别人用的时候才发现这是个什么玩意儿。
今天不整虚的,就聊聊 REST API 设计里那些容易踩的坑。踩过的人都知道,这些坑一旦踩上去,迁移成本有多酸爽。
1. 版本号放 URL 里这事,我曾经深信不疑
当年刚入行的时候,所有教程都告诉你:api.example.com/v1/users,这是标准做法,是最佳实践,是业界共识。我信了,我用了,我后悔了。
为什么后悔?因为 v1、v2、v3 听起来美好,但实际上:
- 每次升级版本,你得维护一堆旧版本代码
- 前端同学要同时对接 N 个版本,心态容易崩
- 你的代码库里 if-else 判断越来越多,像个屎山
更好的做法是什么?用 Header 做版本控制。一根 Accept: application/vnd.example.v2+json 走天下,后端代码干干净净,版本升级只需要新写一套逻辑,旧代码该删删该扔扔。
当然,URL 版本也不是一无是处。如果你做的是公开 API,需要SEO,需要让人家直接浏览器访问,那 URL 版本确实更友好。但如果你做的是内部服务或者移动端后端,Header 版本控制香得多。
2. 命名这事,我见过太多放飞自我的
先来看几个真实案例(我瞎编的,但绝对眼熟):
GET /getUserInfo // ???你不是 REST 吗,GET 不就是拿信息的吗
POST /createNewOrder // 同理,POST 本身就是创建动作
GET /queryAllData // query 是啥,select 呢还是 find 呢
POST /deleteRecord // 用 POST 删数据,你是认真的吗
这些问题本质上是两个:
第一,HTTP 方法语义混乱。GET 就是拿,POST 就是创建,PUT/PATCH 是更新,DELETE 是删除。你把删信用 POST,这波操作我只能说迷惑。
第二,名词和动词混用。REST 的精髓是「名词即资源」,你要的是 GET /users 而不是 GET /getUsers。资源是复数形式(users 而不是 user),动词由 HTTP 方法提供。
正确打开方式:
GET /users # 获取用户列表
GET /users/123 # 获取 ID 为 123 的用户
POST /users # 创建新用户
PUT /users/123 # 全量更新用户信息
PATCH /users/123 # 部分更新用户信息
DELETE /users/123 # 删除用户
简洁、清晰、不废话。这才是 REST 该有的样子。
3. 状态码乱用这事,比 996 还让人难受
HTTP 状态码是 API 的语言,你不能自己发明词汇。以下是高频翻车现场:
- 接口出错了,返回
200 OK然后在 body 里写{"error": "系统繁忙"} - 用户没权限,返回
404 Not Found(怕被发现还是咋的) - 参数校验失败,返回
500 Internal Server Error
讲真,200 只能表示「这个请求我处理完了,没崩」。处理完了不代表成功了。你校验用户密码,返回 200 然后告诉人家密码错误,这是哪门子的 200?
标准做法:
200 OK # 请求成功(不含业务错误)
201 Created # 资源创建成功
204 No Content # 删除成功,无返回内容
400 Bad Request # 参数校验失败、请求格式错误
401 Unauthorized # 未登录或 Token 过期
403 Forbidden # 没权限访问这个资源
404 Not Found # 资源不存在
409 Conflict # 资源冲突,比如用户名已存在
422 Unprocessable Entity # 语义错误,参数格式对但业务上不合规
429 Too Many Requests # 请求过于频繁,被限流了
500 Internal Server Error# 服务器崩了(这个要谨慎用,别啥都甩给 500)
特别说一下 422,这货很多后端不用,但实际上贼好用。422 表示「语法没问题,但我看不懂你的意思」,适合那种「参数类型都对,但值不符合业务规则」的场景。比如 email 格式没问题,但你把别人家邮箱填上了,这就是 422。
4. 分页这破事,我被坑得最惨
「给我加个分页」,这句话我听过不下一百遍。做的时候才发现分页的门道比想象中深。
方案一:Limit/Offset
GET /users?limit=20&offset=100
简单直白,但有个致命问题:数据有删除时,offset 会「跳」。比如你 offset=100 拿第二页,这时候有人删了前十条,你再 offset=100 拿到的就少十条了。
方案二:Cursor(游标)分页
GET /users?limit=20&cursor=eyJpZCI6MTAwfQ==
基于最后一条记录的 ID 做分页,不管中间谁删了啥,数据不会乱。但代价是:你不能随机跳页,只能一页一页往后翻。
方案三:时间戳分页
GET /articles?limit=20&before=2026-09-01T00:00:00Z
适合那种「给我最新消息」的场景,消息列表、动态流什么的。
没有最优解,只有最适合的。你的场景是管理后台那种可以跳着翻的,用 Limit/Offset;你是信息流 Feed,用 Cursor 或时间戳。
5. 错误响应body这事,我发现九成的人写法感人
错误响应 body 该怎么写?大多数人这么干:
{"message": "操作失败"}
{"error": "出错了"}
{"msg": "别问,问就是挂"}
这种错误响应,前端拿到只能弹个「操作失败」,用户看了想打人。
错误响应应该长这样:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
},
{
"field": "password",
"message": "密码长度不能少于8位"
}
]
}
}
为什么要这么复杂?因为:
code给程序看,前端可以根据这个 code 决定展示什么message给用户看,可以直接弹窗展示details给调试看,精准定位哪个字段出问题
你要是只返回一个 message,前端同学每次遇到错误都得来问你:「这个错误码是啥意思?」,你烦不烦?
6. 过度封装这事,做多了比不做还恶心
有些人写 API 喜欢搞抽象,一层套一层:
Controller -> Service -> Repository -> DAO -> Database
五层抽象,听起来很企业级,实际上写个查用户的接口要翻四个文件。代码倒是解耦了,工期也翻倍了,bug 也更难找了。
不是说分层不好,而是要量力而行。小项目非要搞 DDD、搞六边形架构,这不是在写代码,这是在给自己上难度。
我的经验是:
- 简单 CRUD:Controller + Model 够了,别整那些有的没的
- 业务逻辑复杂:加个 Service 层,把业务逻辑拢在一起
- 真的需要换数据源:再加 Repository 抽象
记住,代码是给人看的,不是给架构师表演用的。
7. 文档这事,写了等于没写的太多了
我知道你要说「代码即文档」,我信。但 API 文档这事真不能偷懒。
一个合格的 API 文档长这样:
- 每个接口干什么的,清清楚楚
- 参数类型、是否必填、取值范围,写明白
- 返回值结构、每个字段含义,给出例子
- 错误码对照表,一查就懂
- 认证方式(Bearer Token? API Key?),别让人猜
工具的话,Swagger/OpenAPI 是标配,Apifox 或 Postman 的文档功能也不错。好文档省下的沟通成本,比你写代码的时间还多。
总结一下
说了这么多,其实核心就一句:API 是给人用的,不是给写的人自己欣赏的。
你设计的时候多问自己几个问题:
- 这个名字,别人看能不能猜到是干什么的?
- 这个错误码,用户拿到了能不能知道下一步怎么办?
- 这个分页方式,换一页的时候会不会丢数据?
- 这份文档,前端同学看了要不要来追杀我?
问完这些问题还觉得 ok 的,那这个 API 设计就算合格了。
以上,踩过的坑比走过的路还多,写出来希望你们别重蹈覆辙。各位共勉。