我用一周时间,重构了团队的API设计规范
代码在腐烂之前往往先从接口开始。我接手那个项目的第三周终于被一个诡异的线上事故逼到了墙角——前端调用了GET /user/info后端返回的却是{data: {userInfo: ...}}而另一个服务同样的语义用的是POST /api/getUser。没人说得清哪个是对的因为两套代码分别出自两个已经离职的同事而他们的命名习惯恰好代表了两个时代。我花了一下午翻完仓库里全部三十七个controller发现接口风格至少有五种有REST的、有RPC的、有动词式URL的、还有把SQL直接拼在参数里的。那一刻我意识到团队缺的不是代码规范而是对“API到底代表什么”的共同认知。周一早上我在技术群里扔了一张截图是某个内部服务返回的报错信息{code: 500, msg: 服务器异常}。讽刺的是这个报错来自一个明明应该返回404的接口。群里安静了三十秒然后有人开始甩锅“这是老接口没人动过。”我没接话而是发了一句话“如果我们继续容忍这种接口下个月就会有人写第五种风格出来。”当天下午我拉了一个五人小组宣布要用一周时间重构整个团队的API设计规范。反对声最先来自维护老系统的同事“改规范可以但存量接口怎么办”我回答得很干脆——规范的价值不在于约束存量而在于让增量不再制造新的混乱。其实“重构规范”这件事真正的难点不是写文档而是逼所有人回答一个哲学问题你的接口到底在暴露资源还是在暴露动作团队里默认的说法是“我们做的是RESTful API”但真拆开看大部分人只是把HTTP动词当成了摆设。有人用GET去修改订单状态因为“这样前端用起来方便”有人用POST去查列表因为“参数太长GET怕缓存”。我们把七个核心业务模块的接口全部列在白板上逐个标记它们的“语义——动词——URL——返回结构”结果发现有一半的接口从设计第一天就错了。混乱不是技术债是认知债债主不是代码而是设计者脑子里对“这个接口应该承担什么职责”的模糊。第一天的讨论格外痛苦。后端同事坚持要在URL里写动作片段比如/order/cancel理由是“一眼就能看懂”前端同事则抱怨“你们后端连个统一的返回包装都没有我怎么拦截错误”我提议先放下所有技术偏好只问一个问题如果这个接口被第三方调用对方最想拿到什么没人回答。因为团队从来没想过API会有“外部视角”。这几年大家一直活在内部系统里接口随便写参数随便加反正调用方是同一个公司的人出了问题拉个群就能解决。正是这种“内部系统”的傲慢毁掉了所有规范的可能性。第二天我拿了一份从GitHub上扒下来的Stripe风格API文档放在投影仪上。没有多余的话先让大家看它如何定义错误一个status字段一个error对象里面是type和message所有错误统一结构。然后看它的URL命名全部是复数名词动作全部收敛到HTTP动词。有同事说“这不就是教科书吗”我说“对但我们的问题是从没人愿意翻教科书。”教科书不是给你背诵的是给你在无人可问时当作参照系的。我们决定不照搬任何一家公司的规范而是基于自己的业务模型提炼出三个核心原则第一API的第一公民是资源不是功能第二错误信息必须包含“人可读”和“机器可读”两部分第三任何接口都必须能通过URL反推出它的属性和能力。原则定下来之后真正的挑战才开始怎么把原则翻译成可操作的规则。我让每个人随机挑选当前系统里的一个旧接口按新原则重新设计然后全体投票。有个老哥们选了订单查询原接口是POST /order/query参数直接传JSON返回一个极度复杂的嵌套结构。他新设计成GET /orders?statuspaidpage2返回扁平化的列表每项只含必要字段。大家投票说好他又补了一句“可这样改动前端得全量适配。”我说“那也值得因为你今天不还这笔债明天就得还复利。” 大家笑了但气氛松动了。重构规范不是删掉旧代码而是定义一个新的“默认选项”让以后写接口的人不需要思考就走在正确的路上。到了第三天我们开始制定具体条款。第一件事是统一响应结构我提出用{ok: true, data: ...}或者{ok: false, error: {code, message, detail}}。有个资深后端皱眉“这样所有接口都要包一层性能有损耗。”我说“性能损耗的优先级永远低于认知一致性。如果你的接口响应连个统一的信封都没有那每个调用方都得写一套解析逻辑这才是最大的浪费。” 我们最终定了下来所有正常的业务成功码一律200所有业务失败用4xx/5xx但响应体里的error结构必须保持一致。第四天处理了一个最敏感的问题接口版本管理。旧系统很多接口没有版本号导致不断有人偷偷改字段改完也不通知。我提出必须强制在URL中带上/v1/并且任何破坏性变更必须升到v2。立刻有人说“那v1永远留在那里会很乱。”我反问他“如果你不给旧版本一个合法的存续位置开发者就会在同一个版本里偷偷做破坏性修改那才叫真正的乱。” 版本号不是讨价还价的东西它是对下游的承诺。我们随后在文档里写了一条铁律“任何对输入/输出结构的修改只要导致旧调用方报错就必须视为破坏性变更必须升版本号。”那一天最后我们还定义了一个“扩展字段”的规则新增可选项时如果放到响应体末尾并且加上_ext后缀允许不升版本但必须写入变更日志。第五天的讨论几乎变成了辩论。焦点是“参数校验到底在API层做还是业务层做”。以前团队的习惯是业务层自己校验结果每个接口的报错信息千奇百怪——有的返回参数错误有的返回{code: 40001, msg: xxxx is invalid}还有的直接抛出异常让Spring默认处理返回一坨HTML。我们决定把校验收归到API网关层所有接口入参必须显式声明schema校验失败统一返回422并且error.message要写明具体字段名和约束条件。一个不会告诉你“哪里错了”的API就是在逼调用方用猜的。那一晚我加班到凌晨不是为了写代码而是为了把团队里长期存在的“只要结果对过程无所谓”的思维扳过来。到了第六天我们把草稿整理成了一份完整的规范文档共四章一、资源和URL设计二、HTTP动词与状态码语义三、响应与错误结构四、版本生命周期。但这还不是最终的胜利。因为一份没人遵守的规范还不如一张废纸——所以我做了一个大胆的决定下午找所有相关团队的负责人开了个会要求每个人现场用自己的业务场景尝试违反这条规范看能不能找出现实中不得不违抗的情形。有一个团队说“我们的导出功能要生成ExcelURL里怎么表达”我们讨论后给出了方案POST /exports返回一个ExportJob对象前端轮询GET/exports/{id}下载时再GET/exports/{id}/content。另一个团队说“我们有个内部任务调度器这算资源吗”我们回答“算任务就是资源你可以用PUT去更新它的配置用POST去触发它执行。”世界上没有不能建模成资源的业务只有懒得建模的人。散会时我看到有人眼神里依然有质疑但没有人再反对。第七天我们没有继续讨论技术而是做了一件小事把旧的API文档全部下线在新文档站上挂上了这份规范并附了一个一键检测脚本——它能扫描项目代码自动标记不符合新规则的接口并给出改版建议。当天下午就有同事跑来说“我用脚本跑了一下发现我的接口有17处不符合规范。”他语气里带着沮丧。我说“这恰恰是好消息因为从今天起你有了一份明确的地图而不是在黑暗中蒙着眼睛走路。” 我看着那个数字从17慢慢变成0的过程明白了一周时间到底改变了什么。我们换掉的不是命名规则不是响应格式而是每个工程师在写接口前那一刻的思考方式——从‘我该怎么把数据传过去’变成了‘这个资源应该对外呈现什么状态’。那一周结束后的周例会上技术总监问我这份规范要多久更新一次我说“规范不是纪念碑而是活的操作系统每迭代一次功能就要回头审视它一次。” 他若有所思地点点头。我知道很多人觉得“花一周时间只为了写一份文档”是浪费时间但我的看法完全不同——如果这一周能避免未来无数个深夜的故障排查、避免几十个因为狗屁接口风格引发的吵架那它就是我们做过最值钱的投资。后来新加入团队的实习生问我“为什么要用一周时间来重构API规范而不是直接写代码”我指了指电脑屏幕上那套他刚提交的代码里面有一个接口路径叫/delete_user_by_id。我没有直接批评他只是让他翻开规范手册翻到“资源命名”那一章然后问他“你说DELETE /users/{id}和/delete_user_by_id哪一个更像是一台机器对世界发出的指令”他愣了几秒笑着说“我知道了。”那一刻我确信一周重构的不只是团队的API设计规范更是团队对“专业”这两个字的最低尊重。规范最终会过时但那个因为规范而被纠正的思维习惯会一直留在每个人写下的每一行代码里。