别再写"烂API"了:我在RESTful接口设计中踩过的那些坑
做后端开发这么多年,我见过太多「能用但不优雅」的API了。有些接口你一看就知道是「能跑就行」的作品——命名随心所欲、状态码乱飞、错误信息跟谜语似的。今天不整虚的,跟大家聊聊我在API设计实战中总结出的经验,全是干货。
一、先搞清楚「REST」是什么,别把HTTP当背景板
很多人以为用了GET/POST请求就算是RESTful了,这认知跟「会开车就懂发动机原理」差不多。
REST的核心是资源(Resource)和表述(Representation)。你设计的每个API都应该围绕「资源」来思考,而不是「动作」。
来看看我见过最离谱的接口命名:
POST /getUserInfo.do
POST /deleteUser
GET /queryAllOrders.action
看到这种命名,我的感受是:
「这API是谁设计的?站出来,让我看看你的代码规范文档长什么样。」
正确的做法应该是这样的:
GET /users/{id} # 获取单个用户
GET /users # 获取用户列表
POST /users # 创建用户
PUT /users/{id} # 完整更新用户
PATCH /users/{id} # 部分更新用户
DELETE /users/{id} # 删除用户
看,RESTful的API本身就是自描述的。你不需要看文档也知道这个接口是干嘛的——因为名词已经告诉你了资源是什么,HTTP方法告诉你做了什么操作。
二、状态码不是随便选的,这是门学问
状态码是API的门面,但你真的用对了吗?
我见过太多接口永远只返回200,然后靠code字段来区分成功和失败。这不是不行,但这是「偷懒式设计」。
先来张图镇楼,这是HTTP状态码的分类:
2xx - 成功相关
200 OK # 标准成功
201 Created # 资源创建成功
204 No Content # 成功但没返回内容(常用于DELETE)
4xx - 客户端错误
400 Bad Request # 请求参数有问题
401 Unauthorized # 未认证(没登录)
403 Forbidden # 已认证但没权限
404 Not Found # 资源不存在
409 Conflict # 资源冲突(比如重复创建)
422 Unprocessable # 格式对但语义错
429 Too Many Requests # 请求过于频繁
5xx - 服务器错误
500 Internal Server Error # 程序员背锅
502 Bad Gateway # 网关问题
503 Service Unavailable # 服务挂了
有人会说:「200+code模式也很好用啊,比如code=0表示成功,code=1001表示用户不存在。」
我的观点是:如果你团队小、接口少,这么干没问题。但当你的API要面向外部、或者团队超过3个人,标准化状态码能省大量的沟通成本。
状态码是给HTTP客户端、API网关、监控系统看的,它们可不会解析你自定义的code字段。
三、错误信息要有人话,别让调用方猜谜
我曾经接手过一个遗留项目,接口报错永远返回这个:
{
"code": 1002,
"message": "操作失败"
}
看到「1002」,我需要翻一个Excel表格才知道什么意思。而且「操作失败」——什么操作?失败原因是什么?调用方根本不知道从何下手。
我的错误响应设计原则:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"detail": "ID为 abc123 的用户不存在或已被删除",
"request_id": "req_7f8a9b2c3d4e",
"docs": "https://api.example.com/docs/errors/USER_NOT_FOUND"
}
}
这样调用方可以:
- 用code做程序化处理(比如展示不同的UI提示)
- 用message做快速调试
- 用detail做精确排查
- 用request_id去查日志
- 用docs让调用方自助学习
还有一个容易忽略的点:返回错误的时候,状态码一定要对。用户没登录,你返回200+code=401?对不起,这会让HTTP缓存、监控告警、API网关全部失效。
四、版本管理:你总有一天会面对这个坑
接口上线了,一切正常。三个月后,产品说「要在用户接口里加个字段」,然后你发现现有接口加了字段后,调用方炸了——他们程序里没处理这个新字段。
这就是API版本管理的意义。我的推荐策略:URL版本号。
https://api.example.com/v1/users
https://api.example.com/v2/users
为什么选URL而不是Header?两个字:直观。调试的时候直接在浏览器改版本号,比翻Header方便多了。
版本升级的原则:
- 只在原有字段上做增量,不删除或修改现有字段
- 废弃版本要有明确提示(响应头加Deprecation警告)
- 给调用方足够的迁移时间(至少一个版本周期)
API是契约,改了要通知,不能偷偷摸摸发版就改。这是对调用方的基本尊重。
五、分页:这事儿比你想的重要
「列表接口要不要分页?」——这问题我被问了不下一百次。
答案是:只要列表可能超过10条,就必须分页。
分页方式我推荐Cursor-based分页(游标分页),而不是Offset分页:
# Offset分页(问题多)
GET /users?page=2&page_size=20
问题:
- 数据有新增删除时,会出现数据重复或漏掉
- page数大了之后,数据库OFFSET性能差
# Cursor分页(推荐)
GET /users?cursor=eyJpZCI6IjEyMzQifQ&page_size=20
返回:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6IjE0MzYifQ",
"has_more": true,
"page_size": 20
}
}
Cursor分页的优势:无论数据怎么变,遍历时不会重复也不会漏掉。朋友圈、消息流这种场景,用Offset分页就是给自己找麻烦。
六、字段命名:大小写、下划线还是驼峰?
这个问题团队内部吵过无数次的架。我的建议是:统一就好,没有绝对正确。
但如果你问我偏好,我会选snake_case(下划线命名):
// 我的选择
{
"user_id": "12345",
"created_at": "2026-01-15",
"is_active": true
}
// 反对意见:很多前端团队喜欢camelCase
{
"userId": "12345",
"createdAt": "2026-01-15",
"isActive": true
}
选哪种不重要,重要的是全栈统一。我见过后端snake_case、前端camelCase、中间转一道的代码,那个转换逻辑看着就让人心塞。
写在最后
API设计这事,说难不难,说简单也不简单。它不需要什么高深的技术,但需要有「为调用方考虑」的意识。
你的API就是你的产品。接口设计得好,调用方用着顺手,你也少接电话;设计得烂,每次改版都是噩梦,而且迟早有一天会有人在技术社区发帖子吐槽你的API。
记住:好的API设计是让调用方觉得「这API就该这么用」,而不是「这API怎么这么难用」。
共勉。