RESTful API 设计路上踩过的那些坑,今天全部交代

2026-06-28 8 0

RESTful API 设计路上踩过的那些坑,今天全部交代

做后端开发这么多年,我见过太多团队在 API 设计上翻车。有些是新手不懂,有些是老手惯性思维,还有一些是「大家都这么干所以我也这么干」的从众心理。今天就把这些年亲眼目睹、亲身经历、亲手填过的坑,全部抖落出来。

声明一下:这不是给小白的「什么是 REST」科普,是给已经在做但做得不够好的同行的实战复盘。准备好接受暴击了吗?

坑一:把所有东西都塞进 GET 和 POST

见过最离谱的 API 长这样:

POST /api/getUserInfo
POST /api/deleteUser
POST /api/updateUser
POST /api/listUsers

是的,你没看错,全是 POST。为啥?因为「POST 什么都能传,GET 有长度限制」。我竟无力反驳,只能默默在心里给他点了一根蜡烛。

HTTP 动词是有意义的,别糟蹋它们:

GET    /users        # 获取用户列表
GET    /users/123    # 获取单个用户
POST   /users        # 创建用户
PUT    /users/123    # 完整更新用户
PATCH  /users/123    # 部分更新用户
DELETE /users/123    # 删除用户

REST 不是形式主义,它是让你的 API 自解释的根基。当你看到 DELETE /users/123,你不需要文档就知道这是在删用户。这就是意义。

坑二:URL 命名放飞自我

来看一个真实案例(已脱敏):

/api/v1/userinfo_get
/api/v1/get_order_list_by_user_id_and_date_range
/api/v1/user/123/order/get
/api/v1/userGetInfo

我当时的表情:😱

URL 应该是名词,不是动作描述。动作交给 HTTP 动词来表达。正确的姿势:

GET  /users/123                    # 获取用户
GET  /users/123/orders              # 获取用户的订单
GET  /orders?user_id=123&from=2024-01-01&to=2024-12-31  # 条件查询

几个原则记一下:

  • 用复数名词:/users 而不是 /user
  • 用横杠分隔单词:/user-profiles 而不是 /userProfiles
  • 不要用下划线,看起来像屎
  • 层级结构表达从属:/users/123/orders

坑三:HTTP 状态码乱用

有一种恐怖叫「所有接口都返回 200,然后看 body 里的 code 字段」:

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

HTTP 状态码是干嘛用的?就是让你不用看 body 就知道请求结果。你返回 200 告诉浏览器「一切正常」,然后在 body 里说「其实用户没了」,这不是脱了裤子放屁吗?

状态码使用指南:

  • 200 - 成功(OK)
  • 201 - 创建成功(Created),用于 POST 新建资源
  • 204 - 成功但无内容(No Content),用于 DELETE
  • 400 - 请求参数有问题(Bad Request)
  • 401 - 未认证(Unauthorized)
  • 403 - 已认证但没权限(Forbidden)
  • 404 - 资源不存在(Not Found)
  • 422 - 请求格式对但语义错(Unprocessable Entity)
  • 429 - 请求太频繁(Too Many Requests)
  • 500 - 服务器炸了(Internal Server Error)

有人会说「前端根据 code 做判断也挺好的啊」。好什么好?你的接口被第三方调用,人家看到 200 就认为成功了,谁他妈还去解析你 body 里的 code?到时候线上出问题你就等着哭吧。

坑四:不考虑分页直接 all in

新手最喜欢写的接口:

GET /api/users

然后数据库里有五百万用户。你猜返回什么?

是的,你的服务器会在某个风和日丽的下午突然暴毙,然后 DBA 提着刀来找你。

正确的分页姿势:

GET /users?page=1&per_page=20
GET /users?cursor=eyJpZCI6MTIzfQ&per_page=20

响应结构也要说清楚:

{
  "data": [...],
  "pagination": {
    "current_page": 1,
    "per_page": 20,
    "total": 500000,
    "total_pages": 25000,
    "has_next": true,
    "has_prev": false
  }
}

页码分页适合管理后台这类「需要跳页」的场景。游标分页适合 feed 流这类「持续滚动」的场景。选哪个看业务,别一刀切。

坑五:版本控制拍脑袋

见过最随性的版本控制:

/api/v1/users
/api/v2/users
/api/beta/users
/api/l版/users  (是的,你没看错,有人用过中文)

版本控制是 API 演进的生命线,必须规范。一般用 URL 路径:

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

也可以用 Header,但 path 方式更直观,调试也方便。

版本升级的原则:

  • 新增字段兼容旧格式
  • 删除字段要三思,可以先标记 deprecated
  • 修改字段名不如新增字段然后废弃旧的
  • 不要在一个版本里同时做太多改动

坑六:安全措施形同虚设

最常见的问题:接口裸奔,没有任何鉴权。

有人会说「内网接口不需要鉴权」。内网不会被打穿吗?微服务之间不会串数据吗?等到数据泄露那天你再后悔就晚了。

基础安全配置:

// 1. HTTPS 必须
// 2. 认证信息用 Authorization Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// 3. 敏感操作要校验权限
// 4. 防止 SQL 注入/XSS,别信任何输入
// 5. 请求频率限制
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95

认证方案选型:

  • 短期 Token 用 JWT
  • 长期授权用 OAuth2.0
  • 微服务内部用 mTLS
  • 简单场景用 API Key

坑七:错误信息等于没说

这种错误响应见过吗?

{
  "error": "error",
  "message": "failed"
}

我 failed 你个头啊!这种错误信息对调试没有任何帮助。

好的错误响应应该包含:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "用户不存在",
    "detail": "user_id: 12345 在当前租户下不存在",
    "help": "请检查 user_id 是否正确,或联系管理员",
    "trace_id": "a1b2c3d4e5f6"
  }
}

trace_id 特别重要,配合日志系统可以快速定位问题。线上出问题,用户说「报错了」,你问他「什么错误」,他说「打不开」。有了 trace_id,搜日志一秒定位。

坑八:不做幂等性设计

网络不稳定的时候,前端可能会发起重试。如果你的 POST 接口每次调用都创建一个用户,那用户就喜提一千个小号了。

幂等设计原则:

  • GET、PUT、DELETE 天然幂等,设计时保持这个特性
  • POST 如果涉及创建,用唯一键约束防止重复
  • 支付等关键操作要用幂等 ID(idempotency key)
// 带幂等 key 的请求
POST /orders
Headers: {
  "Idempotency-Key": "unique-request-id-12345"
}

服务端缓存这个 key 一段时间内(建议 24 小时)的响应,重试时直接返回缓存结果。用户体验不受影响,服务器也不会炸。


API 设计这事,说难不难,说简单也不简单。难的地方不在于你知道规范,而在于你能顶住压力坚持规范。当产品说「这个接口特殊一点没事的」,你能说「不,有事的」。

规范不是为了装逼,是为了你的接口在三年后、五个客户端、三百个调用方的时候,还能正常运转,而不是变成一坨没人敢动的屎山。

好了,坑交代完毕。能全躲过去的,算你厉害。

相关文章

从笨拙到默契:我与 OpenClaw 的相爱相杀
从MySQL迁移到PostgreSQL:那些没人告诉你的血泪避坑指南
SQL写得丑,数据库背锅:七个让查询变慢的作死操作
OpenClaw 使用经验分享:这只小龙虾是如何炼成的
OpenClaw 使用经验分享:这只小龙虾是如何炼成的
连接池泄漏的锅,代码居然不背——直到服务器冒烟那天

发布评论