成为全栈·产品篇·契约先行:设计一套被七个端复用的 API 📅 发布时间:2026/8/28 20:22:03 👁 浏览次数: 成为全栈·产品篇·契约先行设计一套被七个端复用的 API本文目标实体定好之后下一步是把它们「暴露」成接口。我会讲清什么是 Contract-First契约先行、统一响应怎么设计、错误码为什么分两层、版本化怎么玩以及为什么这份契约值得用机器去校验。读完你会明白为什么 用一个真实系统串起全栈 说它是「唯一硬地基」。前置知识建议先读 《领域建模领域建模。实体是原料本文是「把原料变成接口」。为什么是「先行」不是「后补」传统做法往往是后端先把接口写出来前端对着跑接口哪里不对口头改文档等有空再补。结局是——文档永远滞后前端拿到的和代码跑的对不上六个端各猜各的。契约先行反过来在写第一行业务代码之前先把 API 用 OpenAPI 定义清楚。这份定义不是文档是七端共同遵守的协议——M1 实现它其余六个端M2–M7共同消费它。谁都不能「随便改改」因为一改六个端一起抖。本系列的契约文件是docs/api/openapi.v1.yaml已经过四轮评审冻结定稿版本 1.11.0。你在 M1 动笔时它已经在那了。一、统一响应包络让前端一次写透错误处理最该统一的是「返回长什么样」。本系列约定列表接口统一返回{ list, pagination }分页参数统一?pagepageSize。你写一个列表渲染组件能套所有列表。详情接口返回单个对象。错误统一结构带一个code字段和业务message详见下一节。为什么要统一因为前端的错误处理最怕「每个接口一套错误形状」。统一之后你可以在拦截器里一次写透所有成功走list/data所有失败读code弹message不用在每个页面特判。这是全栈协作里最划算的一次性投入。统一包络还有一个隐藏好处分页形状固定后你能写一个通用的分页组件套在所有列表上——文章列表、评论列表、后台用户列表UI 一致、逻辑一处。前端最怕的「每个列表重写一遍分页」就此消失。具体长什么样一段 JSON 看明白// 列表统一包络 { list, pagination }{list:[{id:1,title:成为全栈·产品篇·为什么前端要走向全栈,status:published}],pagination:{page:1,pageSize:10,total:137}}// 详情返回单个对象data 包裹便于拦截器统一拆包{data:{id:1,title:...,content:Markdown 源文……,authorId:2}}// 错误HTTP 码归框架业务码归语义{code:3003,message:文章不在 pending 状态无法审批}二、错误码分两层HTTP 码 业务码这是新手最容易混的点。本系列把错误分成两层各管各的第一层HTTP 状态码传输层。由网关/框架负责表达的是「这次请求本身成不成」401没登录、403没权限、404资源不存在、409冲突、429限流、500服务端炸了。第二层业务错误码应用层。放在响应体的code字段表达的是「你的业务逻辑里具体哪一步不对」1002未携带令牌、1003刷新令牌失效、1004令牌无效3002资源冲突如分类下有子节点不能删3003状态转移非法如文章不在pending不能审批为什么不能只用 HTTP 码因为一个400可能对应几十种业务原因前端光看400没法告诉用户「你这个分类下有文章删不掉」。HTTP 码归框架业务码归你的系统语义——两层各司其职前端才能既做通用拦截、又能给精准提示。你可能会反对「小项目哪用分得这么细一个message搞定。」这话对一半。项目小的时候确实可以只返message但「错误码」的真正价值在规模化与多端七个端共用一份契约如果没有统一的code每个端都要自己猜「这个 400 到底是什么意思」而且监控告警、日志聚合都靠code做聚合维度。所以契约先行时把错误码定死是为七端铺路不是为单端锦上添花。这也是为什么我们把错误码收敛成有限的几组语义未登录、权限、冲突、状态非法……而不是漫天飞数字。把这两层上下分开看更清楚三、版本化多端共用不能随便 breaking契约路径统一带版本/api/v1/...。规则很硬向后兼容的变更加字段、加端点、放宽约束直接落在 v1——老端不受影响。破坏式变更删字段、改语义、改错误码含义必须升到/api/v2并保留 v1 过渡期到期再移除。为什么这么较真因为你有六个消费端。你今天在 v1 里把某个字段改名M2、M3、M4、M5、M6、M7 六个端的代码同时编译不过或运行报错。版本化是给「多端共用」上的保险锁——它让契约能演进又不让演进变成屠杀。举个例子某天你发现Article的author字段不够要改成authorId 单独的用户端点避免每次列表都 join 出整个用户对象。这是典型的破坏式变更——老前端代码里article.author.name会直接崩。正确做法是升/api/v2/articles返回新结构v1 保留到所有端迁移完。多端共用逼你敬畏版本。反例很常见某开源 API 在 v1 里把user对象的name字段改名成username没升版本。结果所有老客户端突然拿不到用户名issue 区炸锅作者被迫回滚。一次没走版本化信誉损失比「多维护一个 v1 端点」贵得多。四、机器化契约不是文档是可校验的机器这份契约最不一样的地方是它能被机器校验而且有两道门结构门用openapi-spec-validator检查必须严格符合 OpenAPI 3.1 规范。语法层面不能任性。语义门用check_contract.py跑一组断言当前 33 条全绿例如状态机是否闭环每个状态都有进/出端点、每个 schema 是否被真实引用不能有孤儿实体、46 个授权端点声明是否一致。你可能会问至于吗至于。因为七个端照这份契约写契约错一处六个端集体翻车。把约束下沉成机器断言就是不让「人肉评审漏看」这种事发生。第三轮整改时语义门当场抓出一个没人注意到的孤儿 schemaErrorDetail定义了却没端点引用——这种坑靠人眼永远发现不了。把双门流程画出来五、回到那个根本问题把这一篇和上一篇连起来看《领域建模 你定义了实体和关系数据「是什么」本文你把它们暴露成接口数据「怎么被访问」。这两步合起来就是「设计是常量」的全部内容——实体关系和 API 契约是所有端共享、且一次定对的东西。所以当你纠结「我该先学前端的哪个框架」时记住框架会换实体和契约不会。先把这两样想清楚后面七个端怎么长都是顺理成章的事。所以下次有人问你「会不会写接口」别只答「会调 fetch」。能设计出一份七端共用、机器校验、版本可控的契约才是全栈该有的答案——而这份能力恰恰从 M0 这几篇就埋下了。记住会调接口的人很多能定义接口的人稀缺——而这就是全栈的护城河。小结契约先行先定义 API 再写实现七端共用一份协议谁都不能随便改。统一响应包络{ list, pagination } 统一错误结构让前端一次写透错误处理。错误分两层HTTP 码管「请求成不成」业务码管「业务哪步错」各司其职。版本化/api/v1是多端共用的保险锁破坏式变更才升 v2。契约用双门机器校验结构门 语义门 33 条断言因为它错一处六端翻车。实体 契约本文「设计是常量」的全部框架只是长在上面的端。延伸阅读《领域建模——本文的实体来自那一篇的拆解。{{LINK:M0-06}}《工程公约》——下一篇讲清楚这份契约怎么和代码、文章、git tag 绑定成可复现的整体。地基文档02-领域模型与API契约 docs/api/openapi.v1.yaml——本文所有约定的完整定义与机器可读契约。订阅这个专栏如果你也想跟着一个真实系统从「调接口的人」走到「设计系统的人」欢迎订阅我的《成为全栈开发工程师》专栏。后续每篇都会带着可运行的代码和完整的设计取舍走下来欢迎在评论区讨论、指正。本系列专栏https://blog.csdn.net/fungleo/category_13204651.html订阅看全部篇章完整项目仓库https://github.com/fengcms/become-a-full-stack-developer