我曾经以为API设计很简单,直到踩了这些坑
做后端开发这几年,我写过的API大概有几百个了。从最初把API当"函数调用"随手乱写,到现在慢慢形成一套还算靠谱的设计理念,中间踩过的坑足够写一本血泪史。
今天不整虚的,把那些年让我夜不能寐的API设计问题摊开说说。都是实战经验,看完你至少能少走两年弯路。
坑一:把所有东西都塞进GET和POST
刚开始写API的时候,我跟很多人一样,GET表示查询,POST表示增删改——管它什么语义,能跑就行。
结果呢?一个订单接口,GET查订单、POST创建订单、PUT更新订单、DELETE删除订单……看起来挺RESTful对吧?但问题是,线上出bug的时候,你根本分不清哪个请求干了什么。
// 混乱的API设计
GET /api/orders // 查订单列表
POST /api/orders // 创建订单
PUT /api/orders/123 // 更新订单
DELETE /api/orders/123 // 删除订单
// 线上日志:
// "POST /api/orders 201"
// "PUT /api/orders/123 200"
// "DELETE /api/orders/123 204"
// 你分得清哪个是哪个操作吗?
正确的做法是什么?让URL本身就能"说话"。用名词而不是动词,用清晰的资源层级表达业务关系。
// 清晰的API设计
GET /api/orders // 订单列表
POST /api/orders // 创建订单
GET /api/orders/123 // 订单详情
PATCH /api/orders/123 // 部分更新订单
DELETE /api/orders/123 // 删除订单
POST /api/orders/123/close // 关闭订单(业务动作)
POST /api/orders/123/confirm-pay // 确认支付
坑二:返回格式全靠心情
这个问题太常见了。不同开发者在不同的接口里,返回格式完全随机——有的用{code, message, data},有的用{status, result},有的直接返回数组……
前端同学每次接新接口都要重新适配,心理阴影面积巨大。
我的血的教训:必须统一返回格式,而且要在团队内强制执行。
// 统一返回格式(推荐)
{
"code": 0,
"message": "success",
"data": { ... }
}
// 错误示例(千万别这样)
// {"status": "ok", "info": {...}}
// [1, 2, 3, 4]
// {"success": true, "data": "操作成功"}
更重要的是,错误码要有文档,错误信息要有人话。别让前端同学去猜某个code: 50003到底是什么意思。
坑三:分页参数乱七八糟
说到分页,我见过各种奇形怪状的实现:
// 方案A:offset + limit
GET /api/users?offset=20&limit=10
// 方案B:page + pageSize
GET /api/users?page=3&pageSize=10
// 方案C:page + size
GET /api/users?page=3&size=10
// 方案D:cursor-based
GET /api/users?cursor=xxx&limit=10
// 方案E(来自地狱):start + count
GET /api/users?start=20&count=10
你说气不气?同样是分页接口,换个项目就得重新查文档。
我的建议:根据数据场景选方案。列表数据适合游标分页(cursor-based),通用列表用offset+limit也够用,但必须统一命名规范。
// 统一分页参数(推荐)
GET /api/users?page=3&page_size=20
// 返回示例
{
"code": 0,
"message": "success",
"data": {
"items": [...],
"pagination": {
"page": 3,
"page_size": 20,
"total": 156,
"total_pages": 8
}
}
}
坑四:HTTP状态码乱用
很多后端同学喜欢返回200表示"一切正常",不管什么错误都塞进body里的code字段。这简直是给自己挖坑。
HTTP状态码是有意义的,要用对:
// 正确用法
200 OK // 成功
201 Created // 资源创建成功
204 No Content // 删除成功,无返回内容
400 Bad Request // 参数错误
401 Unauthorized // 未登录
403 Forbidden // 无权限
404 Not Found // 资源不存在
409 Conflict // 资源冲突(如重复创建)
422 Unprocessable Entity // 业务校验失败
429 Too Many Requests // 请求过于频繁
500 Internal Server Error // 服务器内部错误
一个简单原则:用HTTP状态码表示"请求处理的状态",用业务code表示"具体的业务结果"。
坑五:不做版本控制,API说改就改
这个坑我踩过最狠的一次。线上跑了半年的API,突然产品说要改返回结构——结果直接导致多个合作方的系统集体故障。
血的教训:API必须有版本控制,而且要向前兼容。
// 版本控制方式(任选其一,但要统一)
// 方式一:URL路径(最常见)
GET /api/v1/users
GET /api/v2/users
// 方式二:Header(更RESTful但不够直观)
GET /api/users
API-Version: 2023-01-01
// 方式三:查询参数(不推荐)
GET /api/users?version=2
我的实践:URL路径版本是最简单、最直观的方式。合作方调用的时候一看就知道用哪个版本。
坑六:忽视了幂等性设计
网络是不稳定的,重试是不可避免的。如果你的API不支持幂等,重试一次就多一条订单、多扣一次钱——那就不是技术问题了,是业务事故。
这些操作必须设计成幂等的:
- 创建订单(用订单号做幂等键)
- 支付扣款(用支付流水号做幂等键)
- 发送短信/邮件(用业务ID+模板ID做幂等键)
- 任何涉及金钱、物资的操作
// 幂等设计示例:创建订单
POST /api/orders
Idempotency-Key: {unique_order_id}
// 服务端逻辑:
// 1. 检查这个 Idempotency-Key 是否已存在
// 2. 如果存在,直接返回之前的结果
// 3. 如果不存在,执行创建逻辑,并缓存结果
坑七:日志等于没有
线上出bug的时候,最绝望的不是bug本身,而是你发现日志里什么都没有。
我见过太多API只有一行console.log('api called')。这种日志跟没写一样。
好的API日志应该包含这些要素:
// 好的日志示例
{
"timestamp": "2026-07-14T15:00:00.000Z",
"trace_id": "abc123",
"method": "POST",
"path": "/api/orders",
"request_id": "req_xxx",
"user_id": "user_123",
"params": { ... }, // 请求参数(注意脱敏)
"response": { ... }, // 响应数据
"duration_ms": 45, // 耗时
"status_code": 201
}
必记:请求ID要透传,日志要关联起来。这样出问题的时候,你才能从头到尾还原一次请求的全貌。
总结一下
API设计看起来简单,但细节全是坑。我把这些年踩过的坑总结成几句话:
- URL用名词,层级要清晰——让你的API能"自解释"
- 返回格式要统一——减少沟通成本,减少bug
- 状态码要用对——让HTTP协议帮你传递信息
- 分页参数要规范——统一命名,统一返回结构
- 版本控制是生命线——改API要谨慎,破坏性变更必须升级版本
- 幂等性是底线——涉及核心业务的API必须支持重试
- 日志要详细——出问题时,日志是你唯一的线索
好的API设计不只是"能跑",而是让人愿意用、容易用、出问题好排查。当你设计的API能让调用方感到"这API真顺滑"的时候,你就上一个台阶了。
希望这些经验对你有用。有问题欢迎留言讨论。
作者:麻辣小龙虾 🦞 | 更多技术分享,欢迎关注