前端接口契约治理实践:MSW + 错误码 + OpenAPI 的一次落地
背景
在一个中后台项目中,我们遇到了一个典型但长期被低估的问题:
- Swagger 文档先于页面设计完成
- 页面实际需要的数据结构与现有接口不一致
- 错误码基本等同于 HTTP Status,语义不足
- 前端页面中大量
if (res.code !== 0)+toast(res.message)
结果是:
- 前后端对接口结构理解不一致
- Mock 与真实接口行为不一致
- 错误提示不可控,难以国际化
- 后续维护成本持续升高
我们最终决定 重构接口契约的生产流程,而不是继续修补页面代码。
目标
这次调整的目标非常明确:
- 前端可以先定义接口形态
- 后端可以明确对齐的契约
- 错误码具备业务语义,而不是 HTTP 映射
- 页面层不再关心
code、message - Mock / 测试 / 生产行为保持一致
核心决策
1. 使用 MSW 作为接口契约的“前置实现”
我们没有继续使用“纯 mock 数据”的方式,而是把 MSW 当作接口协议的可执行定义:
- 明确 URL
- 明确 request / response 结构
- 明确错误码返回
MSW 的职责不是“造假数据”,而是:
在后端未完成前,冻结接口语义
2. 新建独立契约仓库
我们将以下内容从前端项目中剥离:
- 错误码表
- 通用响应结构
- OpenAPI 文档
形成一个独立仓库,例如:
1 | api-contract/ |
这个仓库的唯一职责:
描述系统“允许发生什么”,而不是“如何实现”。
3. 错误码不再等价于 HTTP 状态码
我们重新设计了业务错误码段:
0:成功1xxx:通用错误(参数、鉴权、限流)4xxx:业务级客户端错误5xxx:服务端错误
例如:
1 | 1002: |
HTTP Status 只用于传输层语义,业务语义只由错误码表达。
4. 统一 ApiResponse 结构
最终我们选择了包装式返回结构:
1 | code: number |
分页数据被视为业务数据的一部分,而不是协议的一部分:
1 | data: |
这样可以避免:
- 多种返回结构并存
- request 层复杂判断
- 泛型难以复用
前端错误处理的收口
request 层是唯一的错误判断入口
1 | if (res.code !== 0) { |
页面层不再判断 code
页面统一使用:
1 | try { |
页面只做三件事:
- 调用接口
- 成功处理
- 必要时对特定错误码做行为响应(跳转 / 弹窗)
错误提示与 i18n 的关系
我们将错误文案分为两层:
通用错误(必须 i18n)
- unauthorized
- forbidden
- invalid_param
- internal_error
这些错误在多个页面、多个接口都会出现。
业务错误(可选 i18n)
- key_limit_reached
- order_already_paid
业务错误优先使用 i18n,如果未配置则使用后端 message 兜底。
MSW 与 OpenAPI 的关系
在这套体系中:
- MSW 是可执行契约
- OpenAPI 是文档化契约
- 后端实现以 OpenAPI 为准
实际流程是:
- 前端用 MSW 定义接口形态
- 将 MSW 对应结构登记到 OpenAPI
- 后端根据 OpenAPI 实现
- 后端生成 Swagger
- 前端可用 Orval 生成类型与请求代码
Orval 生成的 MSW handler 主要用于:
- 自动补齐接口
- 校验契约是否被破坏
- 辅助测试,而不是主 Mock 来源
最终效果
这次调整带来的变化非常直接:
- 页面代码明显变薄
- 错误提示完全一致
- Mock / 真接口无行为差异
- 新接口设计成本下降
- 前后端沟通以“契约 diff”为主
总结
这次实践的核心并不是 MSW、OpenAPI 或 Orval 本身,而是一个观念转变:
接口不是后端“提供”的,而是系统“约定”的
当契约先于实现存在,很多长期存在的协作问题会自然消失。
前端接口契约治理实践:MSW + 错误码 + OpenAPI 的一次落地