API 契约与类型生成

API 契约与类型生成

前端拿到一份接口文档后,通常会再写一遍请求参数和响应类型。只要服务端改了字段、文档忘了更新,或者前端对可空性理解不同,这几份描述就会慢慢分叉。

类型生成解决的就是这部分机械同步,但前提是团队先有一份可信的 API 契约。把不准确的文档交给生成器,只会更快地产生一批不准确的类型。

契约不只是响应字段列表

一份可用的契约至少要说明入口、请求方法、参数位置、字段类型、是否必填和响应结构。错误状态、分页方式、认证要求、幂等约定和版本策略同样属于接口行为,不能只留在聊天记录里。

不同描述方式强调的内容不同。OpenAPI 适合描述 HTTP API 的路径、参数和响应;GraphQL Schema 定义查询图中的类型和操作;Protobuf 等 IDL 则常用于描述 RPC 服务与消息。选哪一种取决于接口本身,而不是生成器支持的语言数量。

契约还需要有明确的所有者。它可以由设计先行,也可以从服务端代码生成,但最后只能有一个权威来源。手工维护文档、服务端类型和前端类型三份事实,比没有生成更容易让人误以为它们始终一致。

生成代码减少的是重复劳动

从契约可以生成 TypeScript 类型、请求客户端、序列化逻辑,甚至用于测试的基础数据。前端不用再逐字段抄写,接口修改也更容易在编译阶段暴露影响范围。

不过,生成出的类型只能描述契约声称会返回什么。服务端部署错误、旧版本节点、代理改写或第三方脏数据仍可能违反契约。对于不可信或影响关键流程的数据,运行时校验依然有价值,这部分放在数据校验与序列化中继续讨论。

生成客户端也不应该渗透到所有组件。生成代码通常贴近传输协议,命名和数据形状会随着契约变化。业务 API 层可以在外面再包一层,用应用里的语言暴露操作,并把生成模型转换为页面需要的模型。这样更换生成器或调整协议时,不必修改每一个调用点。

可空、缺失和错误要写清楚

null、字段不存在和空数组常常代表不同意思。字段不存在可能是旧版本没有返回,null 可能表示明确没有值,空数组则表示集合存在但没有成员。如果契约把它们混为一谈,生成出的联合类型再精确也无法帮助调用方判断业务状态。

错误响应也需要稳定结构。HTTP 状态码可以表达大类,业务错误码用来区分具体原因,字段错误则要能对应到用户输入。只生成成功响应类型,会让前端在失败分支重新猜测服务端返回了什么。

分页和排序规则同样容易遗漏。游标能否重复使用、结果是否有稳定顺序、过滤条件改变后旧游标是否有效,都会影响前端缓存和翻页行为。这些规则很难只靠一个 string 类型表达,需要写进契约说明和测试。

兼容性不能只看能否通过编译

新增可选字段通常容易兼容,但客户端不应该因为看到未知字段就失败。删除字段、把可选改成必填、修改字段类型或改变原有含义,更容易破坏已有调用方。

枚举新增值也值得小心。对服务端来说只是增加一个选项,对前端的穷举分支来说却可能进入从未考虑的状态。客户端应为未知值保留处理路径,生成器也不该让开发者产生“当前枚举永远完整”的错觉。

对 Protobuf 一类按字段编号编码的格式,删除字段后还要避免重复使用原编号;GraphQL 字段则更适合先标记废弃,给调用方留下迁移时间。不同协议的兼容规则不一样,但目标相同:新旧客户端和服务端在发布窗口内仍能互相理解。

生成流程应该可重复

生成器版本、配置和契约文件应进入版本控制或由构建环境固定。否则同一份契约在不同开发机上产生不同结果,提交里的变化就很难审查。

CI 可以先校验契约格式和兼容性,再执行生成,并检查工作区是否出现未提交差异。这样服务端修改契约却忘记更新客户端时,问题会停在合并之前。生成结果是否提交到仓库要看项目发布方式,但无论哪种选择,都要保证构建不依赖某台开发机上的手工步骤。

评审时更该关注的不是生成文件里成百上千行机械变化,而是契约本身改了什么:字段语义是否变化、错误是否兼容、调用方是否有迁移路径。生成代码只是让这些变化更早暴露,不会替团队完成接口设计。