你的REST API正在默默杀人:五个让前端想砍死你的设计

2026-08-20 8 0

你的REST API正在默默杀人:五个让前端想砍死你的设计

我见过太多后端写的API了。有些API,用起来像在吃玻璃渣;有些API,美得像艺术品。前者的作者通常觉得自己在"写接口",后者知道自己是在"设计契约"。

今天不教你背规范,不讲那些网上抄来的人云亦云。就说点大实话,关于那些让前端开发者深夜对着屏幕发呆的烂设计


一、HTTP状态码?你根本不懂

99%的国内项目,接口返回是这样的:

{
  "code": 0,
  "message": "success",
  "data": [...]
}

然后所有的错误处理都写成:

if (res.code === 0) {
  // 好
} else {
  alert(res.message);
}

问题在哪? 你把HTTP协议当摆设。201 Created返回code: -1,404 Not Found返回code: 0配message: "用户不存在"。这是什么?这是对HTTP的状态码体系的侮辱

正确做法:

// 用户创建成功 -> 201
POST /users
Location: /users/12345

// 用户不存在 -> 404,Body里给业务细节
HTTP/1.1 404 Not Found
{"error": "user_not_found", "detail": "uid 12345 does not exist"}

// 参数校验失败 -> 422
HTTP/1.1 422 Unprocessable Entity
{"error": "validation_failed", "fields": {"email": "invalid format"}}

这样前端拿到响应,第一时间就能从协议层面知道出了什么问题,而不是傻乎乎地去解析你那个蹩脚的code枚举。


二、分页:重灾区中的重灾区

你见过多少种分页方式?limit/offset、page/pageSize、cursor/token……每种都有人用,每种都有人用错。

最蠢的设计:

GET /articles?page=1&pageSize=20

然后返回:

{
  "data": [...],
  "total": 1000,
  "page": 1,
  "pageSize": 20
}

为什么蠢?因为假分页。你让前端传page=1,后端默默count(*)查了全表,然后skip了0条取20条。这在大数据量下是灾难。

更蠢的是cursor分页写成这样:

GET /articles?cursor=eyJpZCI6MTIzfQ&limit=20

返回:

{
  "data": [...],
  "nextCursor": "eyJpZCI6MTQzfQ",  // 这啥?
  "hasMore": true
}

nextCursor是个base64 JSON,里面有id、时间戳、排序字段。前端问:这玩意儿怎么解析?后端答:别解析,直接传回来就行。前端内心:……

建议:要么不用cursor,要用就透明。Cursor里有什么字段,在文档里写清楚,或者直接暴露字段:

GET /articles?after_id=123&limit=20

干净利落,前端看得懂,不用在那猜测你base64里藏了什么秘密。


三、嵌套资源:别让你的URL变成文件系统

见过这种URL吗?

GET /orgs/123/depts/456/teams/789/members/101

五层嵌套。这不是REST,这是拍脑袋

资源嵌套是有道理的,但不是无限制地嵌套。超过两层,就要问自己:我是不是在用URL表达关系,而不是在暴露资源?

更好的做法:

GET /members/101?include=team.dept.org

或者:

GET /members/101?fields=id,name,team{id,name,dept{id,name}}

用查询参数控制返回内容的深度,URL保持扁平。这才是正确的API设计思维。


四、命名:你在侮辱程序员的智商

看看这些真实存在的字段名(我发誓不是我编的):

  • userName、username、user_name、UserName——四个接口四种写法
  • createTime、addTime、addtime、ctime——猜猜哪个是你的?
  • isDelete、isDeleted、deleted、deleteFlag——删了还是没删?
  • status状态枚举值:0/1、"正常"/"禁用"/"pending"/"PENDING"——全靠蒙

命名不一致是API的癌症。它不会立刻杀死你,但它会在整个项目生命周期里慢慢放血。

我的原则:蛇形还是驼峰,提前定好,全局执行。 推荐用蛇形(user_name),因为JSON里标准就是下划线,JavaScript那边转驼峰(userName)只是一行代码的事。

枚举值用字符串,不要用数字。数字写进数据库没人看得懂,字符串写进日志一眼就知道是什么状态。


五、版本管理:你的v1可能杀死你

大多数项目的版本管理是这样的:

GET /api/v1/users
GET /api/v2/users

v1和v2有什么区别?不知道。什么时候升v2?不知道。v1什么时候下线?不知道。

结果就是:团队永远不敢动v1,因为不知道哪个客户端在用。v2变成了另一个独立的接口,和v1共享0%的代码,维护两套完全等价的逻辑。

我的建议:

  1. 不要用URL版本(/v1/、/v2/),用Header:Accept: application/vnd.myapi.v2+json
  2. 有明确的废弃政策:v1至少维护X年,提前Y个月通知下线
  3. 小改动不升版本:加了字段不算breaking change,不用新版本

URL版本是最偷懒的版本管理方案,代价是长期的维护噩梦。


写在最后

好的API设计不是"遵循规范"那么简单。规范是底线,不是天花板。真正的好的API是让调用者用得舒服、看得明白、改得放心

每次你写一个新接口,问自己三个问题:

  • 我需要查文档才能调用这个接口吗?(如果需要,说明设计失败了)
  • 这个接口的响应,以后能加字段吗?(如果不能,说明你的设计缺乏扩展性)
  • 一年后我自己来维护这个接口,会骂自己吗?

如果第三个问题的答案是"会",那你今天就该重构它。

别让你的API成为团队最想删掉的代码。

相关文章

你以为 ORDER BY 很快?我用一次血案告诉你什么叫Too Young
Go语言的context:那些年我踩过的坑,比你踩过的键盘还多
写API接口这事儿,比你想象的坑多多了
写API接口这事儿,比你想象的坑多多了
为什么你的服务总是莫名其妙地挂掉?可能不是因为代码烂,而是因为你不懂错误处理
SQL查询优化:为什么你的数据库慢得像在爬?

发布评论