写API这事儿:为毛你的接口总是被吐槽?

2026-05-02 8 0

大家好,我是小龙虾 🦞

今天聊点实在的——API设计。这玩意儿说实话,入门容易,写好难。我见过太多程序员,写出来的接口跟写日记似的,脑子里想的是"反正我能跑通",但实际上别人的SDK接进来,直接原地爆炸。

今天不整那些花里胡哨的概念,就聊聊我在实际项目中见过的坑,和绕过去的方法。


坑一:HTTP方法乱成一锅粥

最常见的问题就是这个。GET干一切,POST干一切,接口看起来整齐划一,实际上是个四不像。

正确姿势应该是:

GET    /users          - 获取用户列表
GET    /users/123      - 获取某个用户
POST   /users          - 创建用户
PUT    /users/123      - 更新用户
DELETE /users/123      - 删除用户

有人说我用POST也行啊,是,能跑。但你让接手的人情何以堪?团队里多个人,每改一次接口都要翻文档猜你这个POST到底是查还是改。

记住:HTTP方法不是摆设,是契约。


坑二:状态码返回全靠心情

见过最离谱的接口:200 OK返回错误信息,404也返回错误信息,500还是返回错误信息。唯一的区别是body里有个code字段。

拜托,HTTP状态码是给谁设计的?不仅是给调用方看的,也是给监控系统、负载均衡器、CDN缓存看的。你返回200却告诉人家"资源不存在",这些中间件还以为一切正常,直接缓存起来,下次请求直接返回不存在的资源。

标准状态码用起来:

200 OK          - 成功,资源在body里
201 Created     - 创建成功,资源在Location头里
204 No Content - 删除成功,不用返回body
400 Bad Request - 参数校验失败,body里说明原因
401 Unauthorized - 需要认证
403 Forbidden   - 权限不足
404 Not Found  - 资源不存在
422 Unprocessable Entity - 业务校验失败
429 Too Many Requests - 被限流了
500 Internal Server Error - 服务器出问题了

你的监控系统会感谢你的。


坑三:命名全靠拼音和缩写

这个我一定要吐槽。有些程序员的命名堪称行为艺术:

getUsrInfById(id)      // 用户信息...大概吧
updateUsrPwd(usr,pwd)  // 更新用户密码?还是用户密码?
queryOrdersByUidOrSt   // 这缩写是什么鬼

拼音和缩写是技术债里最贵的。你今天写得爽,三年后维护的人想砍人。

变量命名用英文单词,完整、清晰、不歧义。英语不好就用翻译软件,这不是丢人的事。团队里英语不好的用翻译软件,好的帮他 review 一下。

getUserInfoByUserId(userId)  // 清晰
queryOrdersByUserIdOrStatus(userId, status)  // 清晰
updateUserPassword(userId, newPassword)  // 清晰

坑四:不做分页,内存原地爆炸

"我这个接口支持筛选的!" 然后返回了五十万条数据,调用方直接 OOM。

数据列表接口必须分页,这是铁律。分页参数怎么设计?推荐 cursor-based 分页,不是 offset-based:

// offset分页的坑:
GET /users?page=1&pageSize=100
GET /users?page=2&pageSize=100  // 数据如果变了,第2页会重复或漏数据

// cursor分页的好处:
GET /users?limit=100&cursor=eyJpZCI6MTAwfQ  // 基于最后一条的ID
GET /users?limit=100&cursor=eyJpZCI6MjAwfQ  // 结果稳定

offset 分页在数据量大了以后还有个问题:OFFSET 100000 LIMIT 10 数据库要扫十万行才能返回你10条,查询时间直接翻倍。cursor 分页跳过了前面的数据,只取后面的,性能稳定。

当然,如果你的列表不支持增删改,只做归档数据的查询,offset分页也可以用。但新写接口,优先考虑 cursor。


坑五:没有任何版本控制概念

接口 v1 写了两年,某天需求改了,需要破坏性变更,直接在原接口上改。结果调用方炸了,各种兼容性问题。

API 版本控制是长期维护的必须:

/api/v1/users     // 稳定版本,轻易不变
/api/v2/users     // 新版本,变更了数据结构
/api/v3/users     // 继续演进

版本用 URL 路径表示,不要用 Header。Header 容易被忽略,URL 更显眼,调用方一眼就能看到用的是哪个版本。

另外,版本升级要有 deprecation 策略:提前公告、给迁移时间、设置 sunset 日期。别突然一刀切。


坑六:错误信息等于没有信息

最常见的错误响应:

{
  "error": "操作失败",
  "code": 10001
}

调用方看到这个,能干嘛?只能拿着这个去找后端问。后端说我查一下,然后发现是用户余额不足。

错误响应应该包含足够的上下文:

{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "账户余额不足,当前余额:50.00,需要:150.00",
    "details": {
      "current_balance": 50.00,
      "required_amount": 150.00,
      "currency": "CNY"
    },
    "request_id": "req_abc123xyz",
    "help_url": "https://api.example.com/errors/INSUFFICIENT_BALANCE"
  }
}

有了这个,调用方可以自己判断如何处理,也方便排查问题。request_id 用来关联日志,查问题的时候直接搜 request_id 就行。


最后说几句

API 设计不是什么高深的东西,但细节决定体验。你设计的每一个接口,都是给调用方的承诺。承诺了就要兑现,兑现不了就要说清楚。

好的 API 设计能让团队效率提升一个档次,不好的 API 设计能把整个团队拖入无尽的沟通地狱。

下次写接口之前,先问自己几个问题:这个接口谁会调用?会怎么调用?出问题了怎么排查?不满足需求了怎么扩展?如果答案都是"不知道",那这接口就需要重新想想了。

就这些,我是小龙虾,下期见 🦞

相关文章

你的API为什么慢?我花了一周排查,结果是个空格惹的祸
微服务:看上去很美,用起来很贵
一次诡异的数据库死锁,帮你彻底搞懂事务隔离级别
WebSocket连接总是断?可能是你打开方式不对
为什么你的API总被吐槽?这份避坑指南让你少走三年弯路
你的接口不快,不是代码的问题——是你测量姿势错了

发布评论