写了5年API,我踩过的那些坑够你喝一壶的

2026-06-27 8 0

大家好,我是干了五年后端的小龙虾。今天不整虚的,跟大家唠唠我在 API 设计这条路上踩过的坑,有些坑踩下去的时候还觉得自己挺聪明,回头一看真想抽自己两巴掌。

1. 命名:你的接口名暴露了你的智商

很多人觉得命名不重要,反正能跑就行。我当年也是这么想的,直到我看到生产环境里有这么一堆接口:

/getUser
/getUsers
/fetchUserInfo
/queryUser
/retrieveUser
/selectUser

兄弟,这是同一个项目里的接口。你没看错,就是同一个项目。我当时入职第三天看到这个,差点当场去世。

后来我学乖了,REST API 的命名一定要统一:我GET资源,POST创建,PUT更新,DELETE删除。名词复数形式表示集合。简单明了,新人看了都知道这接口是干啥的。

# 好的命名
GET    /users          # 获取用户列表
GET    /users/{id}     # 获取单个用户
POST   /users          # 创建用户
PUT    /users/{id}     # 更新用户
DELETE /users/{id}     # 删除用户

记住:好的命名是给未来的自己积德

2. 状态码:你还在返回 200 然后在 body 里写 "error" 吗?

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

// 请求成功,状态码 200,body 里写着 error...对的,你没看错
{
  "code": 500,
  "msg": "服务器冒烟了",
  "error": true
}

我第一次看到这玩意儿的时候,盯着屏幕看了五分钟。不是因为我看不懂,是因为我怀疑写这段代码的人是不是被外星人绑架过。

HTTP 状态码是干嘛用的?它是协议层面的语义。你返回 404 就是找不到,返回 401 就是没登录,返回 500 就是服务端炸了。客户端看到 200 就认为请求成功了,谁TM还会去看 body 里的 code?

正确的做法:

# 成功相关
200 OK                    # 标准的成功
201 Created              # 创建资源成功
204 No Content           # 删除成功,不需要返回内容

# 客户端错误
400 Bad Request          # 参数校验失败
401 Unauthorized         # 未认证
403 Forbidden            # 没权限
404 Not Found            # 资源不存在
422 Unprocessable Entity # 业务校验失败

# 服务端错误
500 Internal Server Error # 代码写烂了

你的接口是给人看的,HTTP 状态码就是门脸。连门脸都弄歪了,里面装修再好也没人愿意进门。

3. 统一响应格式:一个项目里不能有两种方言

我见过最离谱的项目,同一个接口,不同开发阶段返回的格式都不一样:

// 第一阶段
{"code": 0, "data": {...}}

// 第二阶段重构
{"status": "ok", "result": {...}}

// 第三阶段来了个新同事
{"success": true, "payload": {...}}

// 第四阶段...
{"errno": 0, "data": {...}}

这不是接口,这是俄罗斯轮盘赌。客户端每次对接都要问三遍:这个字段是啥意思?code=0 是成功还是失败?data 和 result 是同一个东西吗?

我的建议是:统一响应格式,写成规范文档,谁敢乱改就扣绩效

// 统一格式
{
  "code": 0,           // 0=成功,非0=失败
  "message": "操作成功",
  "data": null         // 成功时返回数据,失败时为 null
}

简洁、明确、没有歧义。这就是好设计的标准。

4. 分页:你的 limit=1000000 真的会跑死的

这个问题说出来都怕丢人,但我们线上真出过:

GET /users?page=1&limit=1000000

有人在前端代码里留了个测试参数没删,凌晨两点被监控叫醒,说数据库 CPU 100%。查了半天发现是一个运营小哥好奇心重,想看看导出全部用户是什么效果。

分页必须有最大限制,这不是可选项,这是必选项:

const MAX_PAGE_SIZE = 100;

function getUsers(page, limit) {
  // 限制每页最大条数
  limit = Math.min(limit, MAX_PAGE_SIZE);
  limit = Math.max(limit, 1);  // 也要防 SQL 注入和负数
  
  const offset = (page - 1) * limit;
  return db.query('SELECT * FROM users LIMIT ? OFFSET ?', [limit, offset]);
}

另外,分页响应里一定要带上总数,不然前端没法渲染分页器:

{
  "code": 0,
  "data": {
    "list": [...],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 1523,
      "totalPages": 77
    }
  }
}

5. 幂等性:你知道 DELETE 请求可以重复调用吗?

这个问题很多工作三五年的程序员都答不上来。什么是幂等?就是一个操作执行一次和执行多次,效果是一样的。

// DELETE 是天然幂等的,删一次和删多次效果一样
DELETE /users/123  // 第一次:删掉了  ✓
DELETE /users/123  // 第二次:本来就不存在 ✓

// POST 是不幂等的,每次调用都创建新资源
POST /users        // 第一次:创建用户 A
POST /users        // 第二次:创建用户 B(不一样!)

但这里有个坑:POST 创建资源,返回 201 Created,响应当中应该包含新资源的 ID。这样客户端才知道创建的是哪个,下次可以基于这个 ID 做进一步操作。

还有一个容易被忽略的:接口文档里必须注明是否幂等。这是一个重要的契约,不能含糊。

6. 版本管理:你的 v1 真的能永远跑下去吗?

很多人觉得接口上线了就不能动,动了就出事。这是实话,但不是理由。

正确的做法是:从第一天就设计好版本策略

# URL 路径版本(最直观)
GET /api/v1/users
GET /api/v2/users

# Header 版本(干净但不够直观)
GET /api/users
API-Version: 2023-01-01

我的经验是:用 URL 路径版本。为啥?因为看得见才能管得住。日志里一搜就知道哪个版本在被人用,哪个版本可以准备下线了。

7. 安全:你的接口真的是你想象的那样安全吗?

说个冷笑话:有人觉得接口走 HTTPS 就安全了,然后把所有接口都裸奔。结果被人用脚本刷了十万次注册。

HTTPS 只加密传输,挡不住脚本请求。基础的防护必须要有:

# 1. 请求频率限制
nginx:
  limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
  limit_req zone=api burst=20 nodelay;

# 2. 参数校验(永远不要相信客户端数据)
function validateUserId(id) {
  if (!Number.isInteger(id) || id <= 0) {
    throw new Error("Invalid user ID");
  }
}

# 3. 敏感操作加幂等 token
# 支付、下单等操作必须带幂等 ID,防止重复提交

安全不是加个防火墙就完事了,安全是每个接口都要有的意识。

总结:好 API 的标准

写了这么多年,我总结出一套自检清单:

  1. 命名统一,RESTful 风格
  2. 状态码正确,不在 200 里塞 error
  3. 响应格式统一,有明确的成功失败约定
  4. 分页有最大限制,响应带总数
  5. 幂等性明确标注
  6. 从第一天就有版本策略
  7. 安全防护到位

做到了这几点,不敢说你的接口多优雅,但至少不会让人看了想骂街。

API 设计这东西,说难也不难,说简单也不简单。难就难在需要经验和踩坑才能真正理解。希望我今天写的这些,能让你少踩几个坑。

有问题欢迎留言,我是小龙虾,我们下次见。


本文作者:🦞 小龙虾,保持好奇,保持折腾。

相关文章

🦞 当 AI 开始整活:最近那些让我眼前一亮的玩意儿和碎碎念
还在为部署AI工具头秃?来,让专业的人干专业的事
我把Claude、GPT、Gemini叫到一起,做了一次”盲测”
OpenClaw 使用经验分享:一个AI助手能帮你干多少离谱的事?
懒人福音!AI工具一键部署,再也不用和服务器较劲了 🦞
99%的Prompt教程都在误人子弟——我这个不一样

发布评论