为什么你设计的API会被前端骂到祖传代码里?
我职业生涯里见过最离谱的API设计,是一个查询用户信息的接口,返回的字段名叫 u_name,而它的上级接口返回的是 user_name。前端问后端为什么不一样,后端说:"这是历史原因。"然后这个"历史原因"就这么在代码里烂了三年。
API设计这事儿,说难不难,说简单也不简单。难的地方不在于你会不会写接口,而在于你知不知道为什么要这么设计。更难的地方在于——你设计完之后,别人愿不愿意用。
今天不聊什么RESTful规范,那玩意儿百度一搜能出来八百篇。我聊点真正在项目里会让你跟前端对线、和后端撕逼、让测试提刀来找你的东西。
一、你的返回格式,正在谋杀前端工程师
先问个问题:下面两种返回格式,你觉得哪种更让人想砸键盘?
格式A:
{ "code": 200, "data": { "user_name": "张三", "user_age": 28 }, "msg": "success" }
格式B:
{ "code": 0, "data": { "name": "张三", "age": 28 } }
如果你觉得都差不多,那恭喜你,你已经是一名合格的后端了——因为你已经习惯了被折磨。但前端看到格式A的时候,心里可能在想:code到底是0还是200?为什么有个msg字段但从来不装有意义的东西?
很多团队的API返回格式是这么来的:看别人家的项目抄一个,缝缝补补又一年。然后code字段有时候是200表示成功,有时候是0表示成功,有时候甚至是"0000"表示成功——因为创始工程师的老家是东北的,觉得0000比较"顺"。
我的建议是:要么统一用HTTP状态码,要么统一用业务错误码,别两边都塞。双重校验除了让日志多打几行,什么用都没有。
还有一个巨常见的坑:null值的处理。某些后端同学返回的用户信息是这样的:
{ "user_id": 10001, "user_name": null, "user_email": null, "user_phone": null }
前端拿到这样的数据,得先判断每个字段是不是null,然后再决定要不要展示。代码写得跟扫雷一样。这种时候,前端心里想的是:你TM就不能把没值的字段直接不返回吗?
还真能。Jackson有个注解叫 @JsonInclude(JsonInclude.Include.NON_NULL),加上去,没值的字段就直接不序列化了。就这么简单。但就是有人不知道,或者知道但懒得加。
二、分页这件小事,能让你跟前端吵三天
如果API设计里有什么话题能让后端和前端产生不可调和的矛盾,分页绝对是Top 1。
常见分页返回长这样:
{ "data": [...], "total": 1000, "page": 1, "page_size": 20 }
看起来挺标准的对吧?但问题来了:page_size是我传的还是你定的?如果我传了page_size=100,你返不返回?如果total里有些记录被软删除了,前端拿到的总数和实际能展示的数量对不上,怎么办?
更骚的操作是,有些接口的page是从0开始的,有些是从1开始的。前端在代码里写了 page - 1,然后某天突然发现某个接口的page本身就是从1开始的,多减了一次。于是用户在某页看到的数据,跟另一页看到的完全是同一批。
我的血泪教训:分页参数必须文档写死,page从0还是从1、page_size最大能传多少、total包不包含已删除记录——这些必须在接口文档里写清楚,而且要跟前端对清楚,不能自己想当然。
还有一种更离谱的"伪分页":后端把limit和offset参数接了,但底层SQL根本没分页,直接 SELECT * FROM users 查出十万条扔给前端,让前端自己用JavaScript做分页渲染。我见过这种代码,当时整个人都石化了。前端加载了三十秒才把页面展示出来,用户以为电脑坏了。
三、接口版本管理:一场关于"什么时候改URL"的血案
当你需要给API做不兼容变更的时候,你会怎么做?
很多团队的选择是:直接改。因为"反正没人用这个接口"或者"这个接口只有我们在用,改了别人跟着改就是了"。然后两周后你发现,某客户的定制化程序在调用你这个接口,而他们的开发早就离职了,现在系统报错了没人能修。
接口版本管理是个老生常谈的话题,但真正做对的团队不多。常见方案有三种:
1. URL路径版本
GET /api/v1/users/10001 GET /api/v2/users/10001
最直观,但很多人吐槽说URL里带版本号不RESTful。我只能说,RESTful这东西,你遵循了不一定对,不遵循也不一定错。实用最重要。
2. Header版本
GET /api/users/10001 Accept: application/vnd.myapi.v2+json
看着很优雅,实际上用起来很恶心。每次发请求都要改Header,某些调试工具还不方便设置。前端同学表示想打人。
3. Query参数版本
GET /api/users/10001?version=2
最不优雅,但有时候意外地好用——特别是在快速迭代的内部接口里。毕竟谁都能轻松试一下 ?version=2 有没有效果。
我的观点是:不要在版本管理方案上过度设计。选一个团队能接受的方案,文档写清楚,执行到位,比什么方案都重要。最怕的是没有版本管理意识,接口想改就改,改完发个群消息说"接口有变更,请知悉",然后让调用方自己猜改了什么。
四、错误处理:你的错误信息,正在浪费别人的生命
你有没有见过这样的错误返回:
{ "code": 500, "msg": "系统异常" }
就四个字。前端拿到这个错误,想展示给用户看,但不知道具体是什么异常。想排查问题,但不知道是哪里的问题。日志里可能有详细信息,但日志在服务器上,前端又不能直接上去翻。
错误信息分层很重要:面向用户的错误提示和面向开发者的错误诊断信息,是两码事。
{ "code": 10001, "msg": "哎呀,服务器打了个喷嚏", "detail": { "error_id": "err_20260816_7a8b9c", "stack": "NullPointerException at UserService.java:45", "suggestion": "请联系管理员,错误ID: err_20260816_7a8b9c" } }
面向用户的信息可以稍微带点人味儿(如果你家公司文化允许的话),面向开发者的诊断信息必须精确。一个error_id能关联到具体日志,这个能力在排查生产问题的时候价值连城。
还有个常见的坑:错误码体系混乱。有些团队code用HTTP状态码,有些用自定义业务码,有些用字符串,有些用数字。1001有时候表示"用户不存在",有时候表示"订单不存在",完全看心情。建议提前定义好错误码规范文档,什么范围表示什么类型的错误,整个团队一起遵守。
五、最容易被忽视的一点:接口文档
如果你的接口文档需要手动维护,那我基本可以保证它跟实际接口会有差距。代码改了,文档忘了更新的概率无限接近100%。
工具选型上,Swagger/OpenAPI Specification是个好东西。虽然UI丑了点,但能跟代码绑定,每次部署自动更新,比什么文档都靠谱。
还有一个更基本的问题:文档格式不统一。同样是描述一个用户对象,有的字段写"用户ID",有的写"用户标识",有的写"uid"。前端看到一脸懵:这个uid和user_id是不是同一个东西?还是两个不同的字段?
接口文档的本质是团队协作契约。契约不规范,执行的时候就一定会有人违约。
写在最后
API设计这件事,说到底是在设计一种契约。契约的核心不是"我怎么实现方便",而是"调用方怎么用方便"。后端工程师容易犯的错,就是站在自己实现的角度想问题,觉得返回什么字段、加什么逻辑都是自己说了算。
但你设计的接口,最终要交给别人去用。而那些"别人",可能会因为你的一个设计失误多写一百行代码,或者因为你的一个奇怪返回格式多排查一整天的bug。
好的API设计不一定让你被夸,但一定能让你少被骂。这个买卖,还是挺划算的。
——小龙虾,公众号「一只野生程序员的日常」,每周更新,拒绝水稿,欢迎来撩。