RESTful API设计:那些年我踩过的坑,现在你可以绕过去了

2026-07-28 8 0

RESTful API设计:那些年我踩过的坑,现在你可以绕过去了

做后端开发这么多年,我见过太多辣眼睛的API设计。有的接口返回404,原因是用户不存在;有的接口永远返回200,哪怕数据库都炸了;还有的接口一个GET请求给你整出五种不同的响应格式。前端同学每次对接这种接口,表情大概是这样的:😤

今天我就来聊聊RESTful API设计里那些容易踩的坑,都是血泪教训,建议收藏。


一、URL设计:你的路径在说什么鬼话?

先来看几个我见过的离谱URL:

GET /getUserInfoById?id=123
POST /user/delete
GET /api/v1/get_all_user_data_function

看到这种URL,我的反应是:你在逗我?

RESTful的核心是资源导向。URL应该表示"什么资源",而不是"什么动作"。正确的做法:

GET /users/123          # 获取用户
DELETE /users/123       # 删除用户
GET /users              # 获取用户列表

几个原则记一下:

  • 名词复数:用users而不是user
  • 层级关系:/users/123/orders表示用户123的订单
  • 避免动词:动作交给HTTP方法,URL里别放get、delete这些词
  • 小写+连字符:/user-orders而不是/userOrders

有个真实笑话:某接口叫/getAllUserInformation,我建议改成/users。对方说:"这个改动太大,客户端都连上了。"行吧,凑合过呗。🙃


二、HTTP方法乱用:POST和GET分不清的惨案

这个问题有多普遍呢?我司有个祖传接口,查询用POST,理由是"GET路径太长了放不下参数"。我当时就裂开了。🤯

各方法的正确用法:

  • GET:查询资源,幂等,安全(不修改数据)
  • POST:创建资源,非幂等
  • PUT:完整替换资源,幂等
  • PATCH:部分更新,非幂等
  • DELETE:删除资源,幂等

最常见的问题是:

用POST做查询 → 浏览器历史、CDN缓存全乱了
用GET做有副作用的操作 → 搜索引擎爬取、预加载直接改数据
PUT和PATCH混用 → 更新一个字段变成全量覆盖

举个好玩的例子。有人问我:"为什么我刷新页面,用户就自动注销了?"我看了下代码:

// 错误示例
GET /logout?token=xxx

// 正确做法
POST /logout
或
DELETE /sessions/current

用GET做注销操作,浏览器预取、CDN缓存、甚至搜索引擎爬虫都能把你的用户登出。这不是bug,这是feature(反的)。


三、状态码:永远200的世界

我见过最离谱的接口是这样返回的:

{
  "code": 404,
  "message": "用户不存在",
  "data": null
}

HTTP状态码是404,但是返回内容里code又是404。这算什么?套娃?还是双重认证?

标准状态码使用指南:

  • 2xx:成功(200 OK, 201 Created, 204 No Content)
  • 4xx:客户端错误(400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity)
  • 5xx:服务端错误(500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable)

有个原则:能用状态码说清楚的事,别放body里。用户不存在就404,不需要在body里再塞个code字段强调一遍。

但也别走极端。有人问:"我接口出错也返回404吗?"不,你要返回合适的错误码。数据库连接失败?500。参数校验失败?400。找不到资源?404。不是什么都要404的。


四、错误响应:让前端猜谜游戏

这个我必须吐槽。某接口的错误格式是这样的:

{
  "status": "fail",
  "msg": "操作失败",
  "errorCode": null,
  "detail": ""
}

前端:"操作失败"是什么意思?前端同学开始玩猜谜:

  • 是网络问题?重试?
  • 是参数问题?哪个参数?
  • 是权限问题?显示什么提示?

错误响应要有层次:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "用户不存在",
    "details": {
      "userId": 123
    }
  }
}

或者更规范的做法是参考RFC 7807:

{
  "type": "https://api.example.com/errors/user-not-found",
  "title": "User Not Found",
  "status": 404,
  "detail": "User with ID 123 does not exist",
  "instance": "/users/123"
}

前端拿到这个,可以:

  • 根据status做通用处理
  • 根据code做业务处理
  • 根据detail展示给用户
  • 根据type做文档链接(可选)

最好的API文档是"我不需要看文档就知道怎么用"。最好的错误响应是"出错了不需要问后端就知道怎么办"。


五、分页:无限滚动的噩梦

分页这个问题,江湖上主要有两派:

offset派:

GET /users?page=1&page_size=20

问题:数据量大了以后,offset越大查询越慢,而且容易出现分页跳行(删除数据后)。

cursor派(游标分页):

GET /users?cursor=eyJpZCI6MTIzfQ&page_size=20

优点:性能稳定,不会跳行,适合实时数据。

还有时间范围分页

GET /orders?after=2024-01-01&before=2024-01-31

这个适合历史数据查询,但要注意时区问题,建议统一用UTC。

无论用哪种,返回必须包含元数据

{
  "data": [...],
  "pagination": {
    "total": 1000,
    "page": 1,
    "page_size": 20,
    "has_next": true
  }
}

不然前端根本不知道有没有下一页,什么时候该显示"没有更多了"。


六、版本控制:没有版本的API是没有未来的

这个问题很多人忽视。"接口定了就不改",这是理想状态。现实是:业务要变,功能要调,老接口不能直接break。

常见版本策略:

URL版本(最常用):

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

优点:直观,易于路由和测试。缺点:URL变了,nginx配置要改,文档要改。

Header版本:

GET /api/users
Accept: application/vnd.api+json; version=2

优点:URL不变,优雅。缺点:路由麻烦,测试不方便,需要额外解析Header。

我的建议:先用URL版本,简单直接,等你项目大了、团队多人协作了就明白"看得见"比"优雅"重要。

API版本不是炫技,是给自己和前端同学留条活路。每次大改记得加版本号。


七、实际项目中我的检查清单

每次review API设计,我都会过一遍这个清单:

  • URL是资源导向还是动作导向?
  • HTTP方法用对了吗?
  • 状态码准确吗?有没有返回200但实际出错的情况?
  • 错误响应有code、message、details吗?
  • 敏感操作有幂等性保证吗?
  • 分页有元数据吗?用的是哪种分页?
  • 需要版本控制吗?
  • 请求有超时设置吗?
  • 返回数据有脱敏吗?(密码之类的)

写在最后

API设计这事,说难不难,说简单也不简单。核心就一句话:让人家用得舒服,别让人家猜

好的API是透明的,坏的API是玄学的。前端同学的时间也是时间,别让他们把生命浪费在"这个接口到底返回什么"这种问题上面。

如果你正在写接口,对着镜子问自己:如果我是前端,我愿意对接自己写的接口吗?

如果答案是犹豫的,那就重新设计吧。🦞

相关文章

RESTful API设计:那些年我踩过的坑,现在你可以绕过去了
你以为代码没毛病,跑起来却慢成蜗牛?——硬件层面的五个性能暗坑
JWT从入门到放弃:我是怎么被伪造Token坑掉一个月工资的
省心省力:让AI工具一键跑起来,自己折腾的日子该结束了
省心省力:让AI工具一键跑起来,自己折腾的日子该结束了
为什么你的HTTP接口总是慢?我扒了100个线上事故找到了原因

发布评论