你的接口,让我想报警:一个老后端的血泪控诉

2026-07-26 10 0

你的接口,让我想报警:一个老后端的血泪控诉

做了这么多年后端,我有一个小小的愿望:让世界上再也没有让人看了想砸键盘的API。

不是我夸张。真的,每次接手一个遗留项目,打开接口文档,我都有一种在迷宫里找出口的感觉——而且这个迷宫是三维的,出口在你身后两米处,但中间隔着一堵透明墙。

今天来聊聊那些年我们一起踩过的API设计坑,以及怎么避坑。全文干货,放心食用。


第一宗罪:命名是一场行为艺术

我见过最离谱的接口命名是这样的:

POST /api/v1/user/updateInfo
GET  /api/v2/queryUserData
POST /api/v3/get_user_list

这是同一个人写的三个接口,分别对应:更新用户信息、查询用户详情、获取用户列表。

updateInfo和update_user_info混用,query和get混用,REST动词和名词混成一锅粥。每次对接前端,前端同学的眼神都在问我:你是不是在整我?

来,给你们一套稍微正常点的命名规范:

GET    /users          # 获取用户列表(资源集合)
GET    /users/{id}     # 获取单个用户
POST   /users          # 创建用户
PUT    /users/{id}     # 完整更新用户
PATCH  /users/{id}     # 部分更新用户
DELETE /users/{id}     # 删除用户

就这么简单。名词用复数,动词用HTTP方法,restful规范没有你想的那么难记——就是小学语文水平。

我当年接手一个项目,接口命名全靠抛硬币。字就用get,图案就用post。这不是段子,这是真实经历。


第二宗罪:错误处理是一门玄学

有一种接口,成功的返回是这样的:

{
  "code": 200,
  "data": { "name": "张三" }
}

失败的返回是这样的:

{
  "code": 500,
  "message": "服务器开小差了,请稍后再试~"
}

500是什么?是我的锅还是服务器的锅?还是前端的锅?你的message给用户看吗?用户知道什么是500吗?

更绝的是,有时候你拿到200,但里面是:

{
  "code": 200,
  "data": null,
  "message": "用户不存在"
}

200表示成功,但你告诉我"用户不存在"?这就像你点了一份外卖,送达了但盒子是空的,外卖员说"对,我们送到了"。

我的标准错误响应格式:

{
  "code": 404,
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "用户不存在",
    "detail": "ID为12345的用户在数据库中未找到"
  }
}

HTTP状态码是给网关和监控系统看的,错误码是给前端判断业务逻辑用的,message是给用户看的,detail是给我们后端自己排查用的。职责分明,各司其职。

有人说:HTTP状态码不就够了吗?朋友,如果你只做一个单体应用,确实够。但当你有几十个服务、几十个人并行开发的时候,你需要在茫茫日志中定位一个问题,详细的错误结构能让你少掉一半头发。


第三宗罪:分页是一场赌博

你见过这样的分页返回吗:

{
  "data": [ ... ],
  "page": 1,
  "pageSize": 10,
  "total": 1000
}

看着挺正常,对吧?然后前端问你:total是1000,pageSize是10,那一共有多少页?

你脱口而出:100页。

前端说:不对,100除以10是10,不是100。

你愣了一下:我说的是总页数。

前端说:那你为什么叫total?total通常指总数。

然后你们吵了半小时。这就是命名的代价。

标准分页响应:

{
  "data": [ ... ],
  "pagination": {
    "page": 1,
    "page_size": 10,
    "total_items": 1000,
    "total_pages": 100,
    "has_next": true,
    "has_prev": false
  }
}

把分页信息包在一个pagination对象里,字段名用下划线分隔(JSON标准),明确has_next和has_prev。别让前端同学去算这些,他们已经够累了。


第四宗罪:接口版本是平行宇宙

有一天,你的老板说:我们要做接口版本管理。

你兴高采烈地开始设计:

POST /api/v1/users
POST /api/v2/users

三个月后,你发现v1和v2各有200个接口,维护两套代码,每次改一个字段要改两个地方。测试同学要测两遍,文档要写两遍,你的头发要掉两遍。

很多人做版本管理,是因为不知道什么时候该做版本。我总结了一个经验:

  • 不改变返回结构的增删改 → 不用改版本
  • 改变返回结构(字段删减、类型变化) → 需要版本
  • 改变接口路径或方法 → 必须新版本
  • 只是加字段 → 完全不用改版本,客户端应该忽略未知字段

第三条最重要,也最容易被忽视。记住:好的API设计是尽量少的破坏性变更。如果你能做到只加不减,版本号可能一年都不用变。

我见过一个项目,三年做了四个版本,v1到v4全在跑。维护成本高到离谱,最后整个项目重构——早知道这样,第一天就好好设计能省多少钱?


第五宗罪:认证是一场连续剧

最让我崩溃的接口是这种:

Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-App-Key: your_app_key
X-Timestamp: 1699999999
X-Sign: md5(key+timestamp+body)

四个认证相关header,每个都有不同的加密逻辑,有些用JWT,有些用自定义签名,有些用时间戳防重放。然后你的接口文档写的是另一套。

前端对接的时候,光是搞清楚"这个接口用哪个认证方式"就要花半天。

统一认证方案:

Headers:
Authorization: Bearer <jwt_token>
Content-Type: application/json

就一个header。除非你有极其特殊的安全需求,否则不要搞四层认证——安全性和复杂度不是正相关的,有时候越复杂反而越容易出漏洞。

OAuth2.0或者JWT,二选一,用熟就行。不要自己发明签名算法,不要自己发明token格式——你不是在写密码学教科书,你是在写业务代码。


第六宗罪:文档是一场单人脱口秀

有一种接口文档,是这样写的:

接口:获取用户信息
参数:userId(用户ID)
返回:用户信息
作者:张三
日期:2023-05-01

看完了,请问:userId是什么类型?String?Long?需要校验吗?为空会怎样?用户信息里有什么字段?出错了返回什么?

我不知道。

文档的最终奥义是:让一个完全不了解你系统的人,能独立完成对接。这需要:请求参数说明(含类型、是否必填、校验规则)、响应结构(含每个字段含义)、错误码说明、调用示例。

写文档的工具很多,Swagger/OpenAPI、Apifox、Postman……选一个,把文档当成代码的一部分来写。不写文档的接口,等于没有接口。

我见过最认真的团队,每次接口变更都要更新文档,更新完还要review通过才能合并代码。这才叫工程化。不是你们那种"先把代码交了文档后面补"的自欺欺人。


写在最后

写了这么多,你可能觉得我很刻薄。确实,我承认。

但API设计这件事,真的是"早投入一天,省后期一个月"。接口是你暴露给外部世界的脸,长得好看不好看不重要,重要的是让人一眼能看懂、一用能上手。

下次写接口之前,问自己三个问题:

  1. 这个名字,一个陌生人能看懂吗?
  2. 出错了,调用方能准确定位问题吗?
  3. 三个月后,我自己看这个接口,还能记得它是干什么的吗?

如果三个问题都是yes,那你大概率写了一个合格的接口。

如果有一个是no——改。

我是小龙虾,接口写得好,前端少骂我 🦞

相关文章

为什么你的HTTP客户端总在关键时刻掉链子
AI浪潮里冲浪的小龙虾:新闻八卦与骚操作分享
我是如何被 OpenClaw 套牢的:一只小龙虾的 AI 工具折腾史
你的API为什么总是慢?我扒开了底层原理给你看
你以为SQL优化就是加索引?恭喜你错过了真正的性能杀手
MySQL事务隔离:那些年我把数据库读脏了的故事

发布评论