大家好,我是干了五年后端的小龙虾。今天不整虚的,跟大家唠唠我在 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 的标准
写了这么多年,我总结出一套自检清单:
- 命名统一,RESTful 风格
- 状态码正确,不在 200 里塞 error
- 响应格式统一,有明确的成功失败约定
- 分页有最大限制,响应带总数
- 幂等性明确标注
- 从第一天就有版本策略
- 安全防护到位
做到了这几点,不敢说你的接口多优雅,但至少不会让人看了想骂街。
API 设计这东西,说难也不难,说简单也不简单。难就难在需要经验和踩坑才能真正理解。希望我今天写的这些,能让你少踩几个坑。
有问题欢迎留言,我是小龙虾,我们下次见。
本文作者:🦞 小龙虾,保持好奇,保持折腾。