为什么你的API设计得像一坨屎,以及如何修复它

2026-09-03 12 0

我见过太多后端写的API,返回格式乱得像打翻了的调色盘。有的接口返回 {code: 0, msg: "success"},下一个接口又返回 {status: "ok", message: "成功"},再下一个直接裸奔 "操作完成"

作为一个被迫在无数个项目里和这些API共存的后端开发者,今天我要把那些年踩过的坑全倒出来。希望你能少走点弯路,毕竟你的接手者(或者三个月后的你自己)会感谢我的。

一、HTTP状态码:别啥都用200

我知道你懒,但能不能别所有响应都返回200?

HTTP/1.1 200 OK
Content-Type: application/json

{"message": "用户不存在"}

用户不存在,你给我返回200?这是几个意思?200的意思是"大哥,这事儿成了",不是"事儿没成但我还是告诉你一声"。

正确的姿势:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": {
    "code": 40401,
    "message": "用户不存在",
    "field": "user_id"
  }
}

常见状态码的正确使用场景:

  • 400:客户端参数有问题,比如必填字段缺失、格式错误
  • 401:没登录或token过期,不是你的错但你得重新登录
  • 403:登录了但没权限,比如普通用户想删管理后台的数据
  • 404:资源不存在,你找的那个用户/订单/商品数据库里没有
  • 409:冲突了,比如重复提交、版本号不对
  • 422:语义错误,参数格式都对但业务上说不通,比如删除已删除的东西
  • 429:请求太快了,被限流了,稍后再试
  • 500:服务器炸了,这个锅后端背

记住了,200只给"操作成功且有数据返回"的情况。如果没数据,返回个空数组 [] 也比返回 null 强,至少前端好处理。

二、统一的响应结构:这是合同,不能随便改

我见过最离谱的一个项目,12个接口有8种不同的响应格式。接手的时候我感觉自己在玩找不同。

强烈建议所有API统一响应结构:

{
  "success": true,
  "data": { ... },
  "error": null,
  "meta": {
    "request_id": "req_abc123",
    "timestamp": 1725225600
  }
}

或者更RESTful一点,把状态码和data分开:

{
  "data": { ... },
  "meta": {
    "code": 200,
    "message": "OK",
    "request_id": "req_abc123"
  }
}

无论哪种方式,整个项目保持一致是底线。你可以选一种,然后写进你们的开发规范里,违反的人请他喝奶茶作为惩罚。

三、错误信息:说人话,别打哑谜

这种错误信息见过没?

{
  "code": -1,
  "msg": "操作失败"
}

操作失败?什么操作?为啥失败?用户看了想骂人,调试的时候你想扔键盘。

好的错误信息长这样:

{
  "error": {
    "code": 10003,
    "message": "订单金额不能小于0,当前值:-50.00",
    "field": "amount",
    "help": "https://api.example.com/docs/errors#10003"
  }
}

说清楚三件事:什么错了、哪里错了、为什么可能错了。如果有帮助文档链接更好,省得前端同学追着后端问。

四、分页:这对前端同学很重要

如果你的API返回列表数据,不做分页迟早出事。数据量大了OOM、接口超时、前端渲染卡死,一整套套餐给你安排上。

标准分页响应:

{
  "data": [ ... ],
  "pagination": {
    "page": 1,
    "page_size": 20,
    "total": 1024,
    "total_pages": 52,
    "has_next": true,
    "has_prev": false
  }
}

注意,total 字段别省。前端做分页器需要知道总页数,没这个你让前端同学怎么算?

另外,分页参数建议用 page + page_size 而不是 offset + limit。offset分页在数据频繁变动的场景下会有重复和遗漏的问题,page分页更稳定。当然,如果你的列表要支持跳页,page更合适;如果只是下拉加载更多,用cursor分页体验更好。

五、版本控制:别让旧版本突然暴毙

你的API v1跑了两年,突然产品说要改数据结构。你说直接改,行,反正没人反馈问题。然后某天突然一堆老用户炸了——因为他们App半年没更新。

API版本控制的基本素养:

GET /api/v1/users/123
GET /api/v2/users/123

新版本上线后,旧版本至少再维护6-12个月(看你的业务周期)。在旧版本上加deprecation header提醒:

Deprecation: true
Sunset: Thu, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2>; rel="successor-version"

让调用方知道该迁移了,也给你自己留条活路。

六、幂等性:这事儿做不好迟早要还

用户网差,连点了两下支付按钮,扣了两次钱。客服电话打爆了。

所有写操作接口(POST、PUT、PATCH、DELETE)都要考虑幂等性。最简单的方案:客户端生成唯一请求ID,服务端做个幂等表:

POST /api/orders
Idempotency-Key: client-generated-uuid-12345

服务端收到请求先查这个key是否处理过,处理过就返回原结果,没处理过就执行并缓存结果。key的有效期根据业务来定,支付类至少24小时。

七、安全:基本功,别丢人

最后说个老生常谈但永远有人踩的:

  • 所有接口鉴权,别裸奔
  • 敏感数据返回前脱敏,手机号、身份证、银行卡别明文
  • 防止SQL注入,参数化查询用起来
  • 限流熔断,高并发来了别连数据库一起带走
  • 日志别记密码和token,出了事你担不起

总结

好的API设计本质是给别人减负,也给未来的自己减负。统一响应格式、合理用状态码、写清楚错误信息、做好分页和版本控制,这几点做好已经能甩开80%的垃圾API了。

剩下的20%靠经验积累,多踩坑、多看别人踩的坑。希望下次我接手别人代码的时候,少看到一些让人血压飙升的设计。

共勉。

相关文章

Go语言defer坑太多?那是因为你没看这篇
后端CRUD之王翻车实录:那些年我们写过的”能用”代码
三次线上事故后,我终于理解了什么叫”空指针恐惧症”
SQL优化:那些你以为用对了但偷偷在拖慢你系统的索引潜规则
连接池翻车实录:我是如何把服务器搞挂的
你的数据库连接池,正在慢慢杀死你的应用

发布评论