别再让你的API文档变成天书了——一个暴躁后端工的忠告
我见过最离谱的API是什么?有一个接口返回的error message是{"code": -1, "msg": "error"}。对,你没看错,就是字面意思。error。你能猜到是哪个字段出错了?不,你猜不到。这个接口的开发者理直气壮地说:前端自己判断啊!前端同学当场就想把键盘扔到他脸上。
这行干久了,你会发现一个规律:代码写得烂的地方,API一定也烂;API烂的地方,沟通一定乱成一锅粥。API不只是接口,它是团队协作的边界线。你设计得好不好,直接决定你的下游同事是坐着喝茶还是加班到凌晨三点。
一、HTTP方法不是你想怎么用就怎么用的
很多人把GET和POST当成万能钥匙,一个接口搞定一切。查数据?POST。删数据?POST。更新数据?还是POST。理由是,这样简单。简单是简单了,等你哪天想做日志、想做缓存、想做接口治理的时候,哭都来不及。
我的忠告是:把HTTP方法当人看。它们有脾气、有个性、有自己的职责范围。你不能因为我觉得这样方便就让一个大学教授去扫地——虽然他能扫,但这是对他身份的不尊重。
标准用法其实很简单:
- GET——看资源,只读,不修改任何东西
- POST——创建资源,比如新建一个用户
- PUT——替换整个资源,全量更新
- PATCH——局部更新,只改我要改的字段
- DELETE——删资源,不用解释吧
有人会说:我全部用POST也行啊,功能一样。功能是一样的,但语义不一样。语义不一样,缓存策略就不一样,日志分析就不一样,接口治理就不一样。你省了五分钟命名的时间,后面要花五十个小时填坑。
二、状态码不是装饰品,是沟通语言
我见过最夸张的一个项目,所有接口无论成功失败都返回200,然后在body里写{"success": false, "code": 500}。code是500但HTTP状态码是200。这位仁兄一定觉得HTTP状态码是建议,不是规范。
HTTP状态码是API和调用方之间的无声对话。你返回403,调用方立刻知道是没权限;你返回404,调用方立刻知道是资源不存在;你返回500,调用方知道这是服务端的问题,不是它的问题。如果你在body里塞一个自定义错误码,调用方得多做一层解析,多踩一个坑。
我的经验是:
- 2xx——事情办成了,安心用
- 3xx——重定向,不到万不得已别用,调用方容易晕
- 4xx——客户端的错,参数不对、权限不够、资源不存在,这类错误要让调用方知道怎么改
- 5xx——服务端的锅,赶紧看日志吧
还有一个坑:不要在2xx的响应体里塞错误信息。有些接口返回200,但body里写着"status": "failed",这是精神分裂。调用方拿到200,以为自己成功了,结果一看body,发现failed,然后又去看code,发现code是个负数——你到底是想让人家用还是不用人家用?
三、命名是API的门面,也是你功力的镜子
我见过这样的接口:
/getData
/doSomeThing
/queryInfo
/handle
/process
我就想问一句:什么数据?什么东西?什么信息?处理什么?这种命名等于没命名,和给人起名叫人是一个效果。
好的API命名应该是自解释的,调用方看到名字就知道这个接口干什么,不需要点进去看文档,不需要问开发,不信你可以试试:
# 差劲的命名
GET /getUserInfo
# 还行的命名
GET /users/{userId}
# 好的命名
GET /users/{userId}/profile
# 更好的命名
GET /users/{userId}/profile?include=roles,permissions
最后这个,include参数告诉你可以扩展查询角色和权限信息,调用方一看就懂,一用就会。这就是好API的样子——文档即代码,代码即文档。
四、分页不是可选的,是必备的
如果你的接口返回的数据没有分页,那它迟早会爆炸。不是技术上的爆炸,是业务上的爆炸——哪天产品加了需求,要查全量用户,然后数据库里躺着十万条数据,你的接口一口气全返回,前端直接卡死,用户骂娘,老板找你谈话。
分页的标准姿势是什么?业界通行做法:
GET /users?page=1&page_size=20
或者:
GET /users?offset=0&limit=20
响应里要带上总数和分页信息:
{ "data": [...], "pagination": { "total": 1050, "page": 1, "page_size": 20, "total_pages": 53 }}
这样调用方知道还有多少页,可以优雅地做分页导航,而不是傻傻地一页一页翻还不知道尽头在哪里。
五、版本控制是生存的保险
你的API不可能一成不变。业务在变,需求在变,你对设计的理解也在变。但如果你直接在生产环境改接口,旧版调用方全崩了,你要么回滚要么连夜通知下游改代码,两种都是噩梦。
从第一天起就把版本号刻进API的骨子里:
GET /v1/users/{userId}
GET /v2/users/{userId}
或者Header里带版本:
Accept: application/vnd.example.v2+json
版本升级的原则是:不破坏性修改。新增字段可以,修改字段名不行;新增接口可以,删除接口不行;可选参数可以,必填参数不行。如果非要breaking change,提前通知,给足迁移时间,别搞突然袭击。
六、错误响应要有人味儿
回到开头那个例子——{"code": -1, "msg": "error"}。这种错误响应等于没响应,调用方拿到手里的信息是零。我是谁?我在哪?我做错了什么?
好的错误响应应该包含:
- 明确的错误码——调用方可以精确识别和处理
- 人类可读的错误信息——告诉你出了什么事
- 请求追踪ID——方便后台查日志
- 可能的话,给出解决方案——比如密码不能少于8位
举个例子:
{ "error": { "code": "VALIDATION_FAILED", "message": "密码强度不足,请使用至少8位,且包含数字和字母", "field": "password", "request_id": "req_8f3kj2h4g5j6" }}
这才是有诚意的错误响应。调用方知道是密码字段的问题,知道怎么改,知道能用什么请求ID去查后台日志。你多做一步,调用方就少踩一个坑。
写在最后
API设计这事,说到底是同理心。你写接口的时候,要想象对面坐着一个人,他对你的系统一无所知,他只想快速拿到结果。一个好的API应该像一瓶好酒——开瓶即饮,不需要说明书。
那些把接口写成天书的人,往往忽略了一个事实:你写的代码会被人骂,你设计的API会被人记恨一辈子。今天你偷的懒,明天全变成别人加班的时间。
所以,下次动手之前,想一想:这个接口,我愿不愿意让我的朋友来调用?如果连我都不愿意,那凭什么让别人用?
共勉。🦞