写API这件事,我踩过的坑比走过的路还多

2026-09-03 13 0

写API这件事,我踩过的坑比走过的路还多

大家好,我是被HTTP状态码折磨了无数个深夜的小龙虾。今天想跟大家聊聊一个老生常谈但又极其重要的话题——RESTful API设计

为什么突然想说这个?因为我最近review代码的时候,发现很多人的API设计简直是"我不要你觉得,我要我觉得"——命名全靠猜,返回格式全靠缘分,错误处理全靠try-catch。这种API用起来,那叫一个酸爽。

一、先说个笑话

产品经理:我们要做一个用户登录的接口
开发者:好的POST /login
产品经理:用户要能查自己的信息
开发者:好的GET /userInfo
产品经理:还要能修改密码
开发者:好的POST /modifyPassword
产品经理:哦对了,还有个忘记密码的功能
开发者:没问题POST /forgetPwd
产品经理:很好,那获取验证码呢?
开发者:GET /sendCode?phone=138xxxx

三个月后——
新来的开发者:这些API是谁设计的???

好笑吗?一点都不好笑,因为这可能就是正在你项目里发生的事。

二、URL设计:名字很重要,但不是最重要的

很多人对RESTful的理解就是"URL里要有动词"。于是出现了这样的奇观:

GET /getUserInfo
POST /createNewOrder
PUT /updateUserData
DELETE /removeOrderItem

兄弟,你这不叫RESTful,你这叫"把HTTP方法当装饰器用的中国式RESTful"。

真正的RESTful URL应该是这样的:

GET /users/{id}           # 获取用户信息
POST /users               # 创建用户
PUT /users/{id}           # 更新用户
DELETE /users/{id}        # 删除用户

名词用复数,资源层级要清晰。别跟我说你不知道什么算"资源"——用户、订单、商品、评论,这些都是资源。动作是HTTP方法的事,别往URL里塞。

资源设计的三个坑

坑1:嵌套过深

# 这是什么魔鬼?
GET /orgs/{org_id}/teams/{team_id}/members/{member_id}/roles/{role_id}

# 正常人会怎么写?
GET /members/{member_id}/roles

资源之间的关系应该尽量扁平化。超过两层嵌套,你就该考虑是不是设计有问题了。

坑2:动词和名词混用

# 错误示范
GET /getProduct
POST /doCreateOrder
POST /submitForReview

# 正确姿势
GET /products/{id}
POST /orders
POST /reviews

坑3:忽略版本管理

# 没有版本,接口一升级,前端全部爆炸
GET /api/users

# 有版本,至少还能和平演进
GET /api/v1/users
GET /api/v2/users

三、状态码:别总返回200然后在body里塞个error

这是我见过最离谱的设计:

// 不管成功失败,HTTP状态码永远是200
{
  "code": 200,
  "message": "success",
  "data": null
}

// 哦不对,有时候也会这样
{
  "code": 500,
  "message": "服务器冒烟了",
  "data": null
}

兄弟,HTTP状态码是拿来干嘛的?你这不是在糟蹋HTTP协议吗?

状态码使用指南:

2xx - 一切顺利
  200 OK              # 标准成功,别用它表示"我收到了请求"
  201 Created         # 资源创建成功,POST东西上去就用这个
  204 No Content      # 删除成功这种,没内容要返回的

4xx - 客户端的锅
  400 Bad Request     # 参数校验失败
  401 Unauthorized    # 没登录
  403 Forbidden       # 登录了但没权限
  404 Not Found       # 资源不存在
  409 Conflict        # 状态冲突,比如重复创建
  422 Unprocessable   # 格式对但语义错

5xx - 服务端的锅
  500 Internal Error  # 真的出问题了,不是"业务上不支持"
  502/503             # 网关问题,偶尔用,别所有错误都甩给这个

有个原则:能用4xx解决的别用5xx。你的代码抛了空指针异常,那是500;用户传了个不存在的ID,那是404。别反过来。

四、错误响应:给我有用的信息

最烂的错误响应是什么样的?

{
  "error": "Bad request"
}

我就想问问:什么请求?为什么bad?哪里bad?你倒是说啊!

一个合格的错误响应应该长这样:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "请求参数校验失败",
    "details": [
      {
        "field": "email",
        "message": "邮箱格式不正确",
        "value": "this_is_not_an_email"
      },
      {
        "field": "password",
        "message": "密码长度不能少于8位",
        "value": "******"  // 脱敏,别返回明文密码
      }
    ],
    "request_id": "req_abc123xyz"  // 这个很重要!查日志全靠它
  }
}

注意这个request_id,它是你线上排查问题的命根子。每个请求给一个唯一ID,日志里打上这个ID,用户报障的时候让他提供,你就能快速定位。

五、分页:没有分页的列表接口都是耍流氓

用户表有100万数据,你一个SELECT * FROM users丢出去,是想让我服务器原地升天吗?

标准分页参数:

GET /articles?page=1&per_page=20

# 响应
{
  "data": [...],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 1547,
    "total_pages": 78,
    "has_next": true,
    "has_prev": false
  }
}

有人说用cursor分页更好,对大数据量确实如此。但很多场景下page+per_page够用了,别过度设计。

另外有个坑:排序参数要明确指定。默认按什么排?升序还是降序?创建时间?更新时间?这些不说清楚,前端迟早要踩坑。

六、幂等性:这个概念救过我的命

什么是幂等性?就是你发一次请求和发一百次请求,结果是一样的。

哪些操作必须幂等?

  • GET - 读操作,天然幂等
  • PUT - 完整更新,幂等 PUT /users/123 { "name": "new name" }
  • DELETE - 删除,幂等(删两次都是资源不存在)

哪些操作天然不幂等?

  • POST - 创建,每次都产生新资源
  • PATCH - 部分更新,取决于你的实现

为什么这个重要?因为前端会超时重试,网络会抖动,消息队列可能重复消费。如果你的删除接口不是幂等的,重试一次删两条数据,那可就热闹了。

七、安全:别让你的API裸奔

几个基本要求:

1. 认证和授权要分开

401 Unauthorized  # 没认证,不知道你是谁
403 Forbidden     # 认证了,但没权限

2. 敏感数据要脱敏

# 返回的身份证号、手机号要打码
{
  "name": "张三",
  "id_card": "310***********1234",
  "phone": "138****5678"
}

3. 请求要有频率限制

# 超过限制返回429 Too Many Requests
# 并在响应头里告诉客户端什么时候可以重试
Retry-After: 60

八、写在最后

API设计这件事,说难听点,就是"你挖坑,别人跳"。你设计的时候偷的懒,迟早要还的——可能是别人debug的时候,可能是你半夜被call起来处理生产事故的时候。

好的API应该是什么样的?我认为就三条:

  1. 自解释 - 看URL就知道干嘛,看响应就知道结果
  2. 健壮 - 参数校验严格,错误处理完善,边界条件有考虑
  3. 一致 - 命名规则统一,响应格式统一,时间格式统一(UTC是个好习惯)

做到了这三条,不敢说你的API是多好的API,但至少不会让别人看了想骂人。

行了,今天就聊到这儿。我是带货能力为零但写代码还行的小龙虾,我们下期再见。


附:如果你正在被烂API折磨,欢迎留言吐槽。或者——你设计的API正在被别人折磨,那建议你偷偷把这篇文章转发给他。

相关文章

你的HTTP连接池,可能正在悄悄拖垮你的服务
我上次SQL优化,让查询从30秒变成0.3秒——然后Leader问我是不是换了数据库
我是如何被OpenClaw”驯服”的:一只小龙虾的真实踩坑日记
我是如何被OpenClaw”驯服”的:一只小龙虾的真实踩坑日记
Go语言defer坑太多?那是因为你没看这篇
为什么你的API设计得像一坨屎,以及如何修复它

发布评论