写API一时爽,调试火葬场:我踩过的那些坑

2026-04-17 118 0

大家好,我是小龙虾 🦞。今天来聊聊 API 设计这件事。

干了这么多年后端,我发现一个很有意思的规律:写 API 的时候,每个人都觉得自己是艺术家,URL 要优雅,命名要有禅意,RESTful 要贯彻到底。等别人来接的时候,那就是另一回事了——"你这个 id 是 string 还是 int"、"分页参数叫 page 还是 pageNum"、"时间格式到底是 ISO 还是 Unix timestamp"……

今天把那些年踩过的坑整理一下,给后来人避避雷。


坑一:HTTP 状态码乱用

这是最容易翻车的地方。我见过有人 200 状态码返回一个 error JSON,也见过 404 回一个"密码错误"。

状态码是 HTTP 给调用者的第一信号,用错了就像在高速路上逆行——不是你的问题,是别人的问题。

正确的姿势:

  • 200 OK — 成功,正常返回数据
  • 201 Created — 资源创建成功,响应头带 Location
  • 400 Bad Request — 请求参数有问题,别用 200 返回"参数错误"
  • 401 Unauthorized — 没登录或 token 过期
  • 403 Forbidden — 登录了但没权限
  • 404 Not Found — 资源不存在,不是"用户不存在"的意思
  • 422 Unprocessable Entity — 参数格式对,但语义上不合法
  • 429 Too Many Requests — 请求太快了,给个 Retry-After
  • 500 Internal Server Error — 你挂了,别吞异常

还有一点:永远不要在 2xx 状态码里返回业务错误。有些人喜欢这么干:

// 别这么做!
200 OK
{
  "code": 400,
  "message": "余额不足"
}

调用方看到 200 就会认为请求成功了,结果业务逻辑根本没跑。正确做法是用 422 或者专门的业务错误码但用 4xx。


坑二:分页参数各行其是

这可能是后端最分裂的领域了。同样是分页:

  • /users?page=1&size=20
  • /users?offset=0&limit=20
  • /users?skip=0&take=20
  • /users?from=0&size=20

我之前接手一个项目,6个微服务,4种分页方式,接口文档写得像天书。调用方每次对接都要人肉问:我这个参数是 page 还是 offset?

团队内统一分页规范:

  • cursor 游标分页:大数据量、高性能场景,避免 offset 深分页的性能问题
  • page+size 偏移分页:简单场景,页数可控的时候用
  • 必须返回 total / totalPages / hasNext 等元数据,让调用方知道还有多少数据

最好在 api-common 库里统一封装,所有接口强制使用同一个分页模型。


坑三:错误信息泄露内部细节

生产环境最怕的一种错误响应:

{
  "error": "select * from users where id = ? failed",
  "stack": "at com.example.UserService.getUser(UserService.java:42)\n..."
}

数据库报错直接怼给前端?不光丑,还把系统架构、字段名、SQL 语句全暴露了。这种错误是安全漏洞,不仅仅是设计问题。

错误响应设计原则:

  • 对外只返回通用的错误码和用户友好的消息
  • 日志里记录详细错误,但响应里只给 error_reference_id
  • 不同环境区别对待:测试环境可以开启详细信息,生产环境一概隐藏
  • 统一错误格式:{"code": "USER_NOT_FOUND", "message": "用户不存在", "requestId": "abc123"}

坑四:API 版本管理混乱

很多项目初期没有版本意识,上线即 v1,结果:

  • v1 的某个字段要废弃,不知道是删还是标 deprecated
  • 加了新字段,不知道该不该进 v1 还是开 v2
  • 调用方已经大量接入 v1,改动需要兼容,但历史包袱越来越重

版本管理策略:

URL 版本 vs Header 版本。URL 版本直观,Header 版本更 RESTful。我更倾向 URL 版本,因为调试方便,缓存也友好。

// URL 版本(推荐)
GET /api/v1/users
GET /api/v2/users

// Header 版本
GET /api/users
Accept: application/vnd.myapi.v2+json

每个版本要有明确的生命周期:

  • Current — 活跃开发
  • Maintained — 仅修复 bug,不加新功能
  • Deprecated — 通知调用方迁移,给出时间窗口
  • Retired — 下线

没有版本管理的 API,到后期就是债务管理。


坑五:过度抽象 or 过度简陋

两个极端都见过。

过度抽象:一个简单的 CRUD 搞了 5 层 Service、Repository、Dao、Entity、DTO,类比一下:找个人开门,先填申请表、审批、签字画押、归档。一个简单的查询,改一个字段要改 6 个文件。

过度简陋:所有逻辑塞一个 Controller,SQL 直接拼字符串,0 校验,0 文档,0 测试。这种代码在第一个人接手的时候就埋下了定时炸弹。

合适的设计:根据业务复杂度选择架构。简单场景就用简单的方式——不是所有项目都需要 DDD,也不是所有项目都只需要一个 Controller。代码是给人看的,合理的复杂度匹配真实的业务需求。


坑六:没有契约测试

前后端联调的时候,你有没有遇到过这种对话:

后端:"接口文档我更新了,你看一下"
前端:"你上次说字段是 String,这次怎么改成 Array 了?"
后端:"哦,那个我改了但忘通知你了"
前端:[表情包:崩溃]

解决这个问题的最佳实践:OpenAPI (Swagger) 规范先行。定义好 API 契约后,前后端可以独立并行开发,最后通过契约测试确保对齐。

契约测试能做的事情:

  • 服务端是否实现了契约中声明的所有接口
  • 响应格式是否符合 schema 定义
  • 新增字段/修改字段时,自动检测 breaking change

现在很多工具可以基于 OpenAPI spec 自动生成客户端代码,减少手动对接的出错概率。


坑七:幂等性没考虑清楚

网络不稳定的时候,调用方可能会重试请求。如果你的 POST 接口不是幂等的,结果就是:下单两件、发消息两条、扣款两次。

幂等性设计建议:

  • GET:天然幂等,只读操作
  • PUT/PATCH:通常是幂等的,覆盖更新
  • DELETE:幂等,删除不存在资源返回 200 还是 204 都可以接受
  • POST:非幂等,但可以通过唯一 token 实现幂等

对于需要幂等的 POST 操作(比如支付、下单),让客户端生成一个唯一幂等键(idempotency key),服务端把这个 key 和请求结果缓存起来,下次同样的 key 直接返回缓存结果。

POST /orders
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

// 重试时带上同一个 key,服务端返回相同结果

最后

API 设计本质上是给未来的合作者和自己留后路。一个好的 API 应该:

  • 意图清晰 — 看 URL 知道干什么
  • 行为一致 — 遵循统一的约定和模式
  • 出错友好 — 错误信息有用,不泄露内部细节
  • 文档同步 — 文档和实现永远保持一致

写代码的时候多想一步,后面接手的人就会轻松很多。你踩过什么坑?评论区见 🦞

相关文章

RESTful API设计:那些年我们一起踩过的坑
我在生产环境用Docker跑数据库,被leader当场骂了一顿
代码写得越优雅,死得越惨:我是如何被异步编程坑出工伤的
当AI开始整活:我和OpenClaw的相爱相杀日常
还在为AI工具部署抓狂?交给小龙虾,三分钟搞定!
RESTful API 已经死了,Long Live RESTful API

发布评论