目录

规范优先的 API 开发

规范优先这件事真正有价值,不是因为它“更规范”,而是因为它能把 API 的关键决策提前暴露出来。

更具体地说:

很多 API 真正昂贵的问题,不是实现难,而是接口一旦被别人依赖,后面就很难改。

如果 spec 是在代码写完之后补的,那很多高成本错误其实已经定型了。

所以这篇文章不打算再讲“Swagger 可以生成好看的文档”这种入门话术,而是讲一件更现实的事:在一个要长期被依赖的 API 里,spec-first 到底怎么落地才有用。

本文默认使用 OpenAPI 3.1。到 2026 年,这已经是新 API 设计里比较正常的基线了。

什么叫规范优先,什么不叫

规范优先的意思是:

  • API 契约先设计
  • 先评审
  • 先版本化
  • 然后实现再去贴这个契约

等于:

  • 一开始就把未来三年的接口全写完
  • spec 写一次永远不改
  • 代码生成能代替工程判断
  • 前后端和产品经理只靠 YAML 远程扔需求

如果团队把 spec-first 做成一种“文档合规动作”,那它会很快失去生命力,而且是也是自然的结果。

真正用得好的 spec-first,通常会带来这些收益:

  • API 设计评审更早发生
  • 兼容性问题更早暴露
  • 前后端可以更早并行
  • mock 和契约测试更容易做实
  • 发布和变更记录更清楚

为什么现在建议直接用 OpenAPI 3.1

OpenAPI 3.1 的意义,不是“版本号更新了”,而是它把很多以前 awkward 的地方理顺了。

实用层面的好处主要有:

  • 和现代 JSON Schema 更贴近
  • schema 表达能力更自然
  • 校验行为和生态工具更统一
  • 少很多“为了兼容旧规范而拐弯”的写法

如果你们公司因为老系统包袱还在用旧版 OpenAPI,那可以理解。
但新 API 如果没有工具链阻塞,直接用 3.1 会更省心。

spec-first 真正重要的场景

不是所有 API 都一定要把 spec-first 做得很重。
它的价值在这些场景里尤其明显:

  • API 有多个消费者
  • 前后端要并行开发
  • 对外开放给客户、合作方、第三方
  • 后端还没完成,前端就要先联调
  • 兼容性要求明确存在
  • 变更需要可追踪、可评审

如果只是某个临时内部接口,一个服务只给另一个服务自己人用,也不是说 spec-first 没价值,只是收益没那么大。

别一上来就写 schema,先把交互流程想清楚

很多团队做 API 设计时,一个典型问题是:
字段还没想清楚业务语义,就已经开始写 schema 了。

这种顺序很容易把问题做歪:

  1. 先列一堆对象模型
  2. 先开始争字段类型
  3. 最后才发现接口行为本身就没想明白

更靠谱的顺序应该是:

  1. 先定义业务流程或用户流程
  2. 再识别资源和操作
  3. 再定义成功与失败语义
  4. 最后再收敛 request / response schema

比如,在争论字段名之前,应该先回答这些问题:

  • 这是同步接口还是异步接口?
  • 创建动作返回最终资源,还是返回一个 operation handle?
  • 客户端允许重试吗?
  • 幂等性怎么定义?
  • 分页过程中数据变化了怎么办?
  • 哪些失败是可重试的,哪些不是?

这些问题,比字段到底叫 createdAt 还是 created_at 更影响长期质量。

设计评审应该基于 spec,而不是基于页面截图

规范优先最重要的地方之一,就是它提供了一个可以评审的契约载体。

一次有用的 API 设计评审,至少应该覆盖:

  • 接口的目的是什么
  • 主要消费者是谁
  • 资源命名是否清晰
  • method 语义是否一致
  • 鉴权模型是什么
  • 分页、过滤、排序规则是否稳定
  • 错误结构是否统一
  • 幂等性是否定义清楚
  • 兼容性边界是什么
  • 有没有足够真实的请求/响应示例

评审不应该只变成“YAML 能不能过 lint”。
能过 lint 只是说明格式没写坏,不代表设计本身没问题。

真正要评的是:

  • 我们到底要求客户端依赖什么?
  • 我们是不是正把某些错误设计永久化?

兼容性规则必须写下来,不能靠默契

很多团队嘴上都说“我们很重视 backward compatibility”,但真问一句“什么叫 breaking change”,往往说不清。

一个比较实用的 API 兼容性规则,通常至少要明确这些:

一般算兼容

  • 新增可选字段
  • 新增新的 endpoint
  • 新增可选 query 参数
  • 在语义不变前提下适度放宽部分校验
  • 某些允许忽略未知值的场景里增加 enum 值

很可能是 breaking change

  • 删除字段
  • 重命名字段
  • 修改字段类型
  • 把 optional 改成 required
  • 改分页语义
  • 改错误结构
  • 在没说明的情况下修改默认排序
  • 修改鉴权要求
  • 结构不变但语义变了

这类规则如果不落文档,最后通常就会变成“大家都觉得自己没改坏”,然后消费者线上出问题。

如果能在 CI 里做兼容性检查更好,但前提还是先把规则说清楚。

示例不是装饰品,而是契约的一部分

很多 spec 技术上完整,但示例写得很敷衍,结果可用性大打折扣。

一个好的 example 作用非常大:

  • 能让语义一眼看明白
  • 能暴露命名上的牵强
  • 能提前看出边界情况有没有漏
  • 能让前端、SDK、测试更早启动
  • 能让 mock 真正有价值
  • 能显著提升自动生成文档的可读性

反过来,差的 example 通常有这些特点:

  • 太小
  • 太理想化
  • 和 schema 自己都对不上
  • 明显不是从真实使用场景里长出来的

建议至少给这些场景写 example:

  • 正常成功返回
  • 空结果
  • 参数校验失败
  • 鉴权失败
  • 分页续页
  • 异步任务处理中 / 完成 / 失败

没有示例的 spec,不是不能用,但会明显难用很多。

错误结构值得单独认真设计

很多 API 在 happy path 上写得很认真,一到错误返回就随手拼个 message。
这在生产里是很糟糕的习惯。

因为客户端通常最依赖的,不是成功响应,而是失败时怎么判断下一步。

一个面向生产的错误契约,至少应该定义:

  • 统一的顶层错误结构
  • 稳定的 machine-readable error code
  • 面向人看的 message
  • 需要时的字段级错误明细
  • trace / correlation id
  • 可重试提示(如果场景需要)

客户端应该能明确区分这些情况:

  • 请求结构不对
  • 未登录
  • 无权限
  • 资源不存在
  • 状态冲突
  • 触发限流
  • 下游暂时失败
  • 异步任务还没完成

不要让消费者去猜字符串含义。

代码生成很好用,但边界一定要守住

很多团队做 spec-first,很快就会走到 codegen。
这一步很有价值,但也很容易遇到问题。

比较适合生成的部分通常是:

  • typed client / SDK 骨架
  • request / response model
  • server interface stub
  • 基础校验代码
  • mock / docs 相关产物

不太适合直接交给生成器统治的部分通常是:

  • 生产级业务逻辑
  • 持久化模型
  • 权限判断
  • 复杂工作流
  • 一边手改、一边还想安全重生成的服务端代码

经验上,codegen 最有价值的边界是:

让它负责“重复而契约化”的部分,业务语义和行为判断仍然由工程代码自己承担。

如果团队开始把“生成出来的代码结构”当成服务架构本身,那基本很快会反噬。

Mock server 的价值,是让前后端不再互相卡死

spec-first 最直接的收益之一,就是后端没完全写完之前,前端和消费者也可以先动起来。

一个真正有用的 mock setup,应该能支持这些事情:

  • 验证 route 形状
  • 验证 auth header
  • 验证 request payload
  • 验证 response schema
  • 提前处理空态、错态、分页态、异步态 UI

但 mock 能不能有用,取决于 spec 是否足够具体。

如果 spec 只写成“200 返回 object”,那 mock server 只是表演,不是基础设施。

契约测试是防止 spec 漂移的关键

spec 如果不对实现做约束,它迟早会漂。

至少要有两类约束意识:

Provider 侧

现在跑着的服务,是否仍然符合这个契约?

Consumer 侧

关键消费者依赖的字段、状态码、错误语义,有没有被悄悄改掉?

这不需要什么主观猜测流程,它只需要团队接受一个事实:

spec 不是展示用的文档,它应该是可执行、可验证的产物。

一旦有多个客户端、多个实现方、或者对外 API,这一点就尤其重要。

Changelog 和发布纪律,比很多人想的更重要

spec-first 只有在“变更也被认真管理”时,价值才能持续。

至少应该记录清楚:

  • 改了什么
  • 是否兼容
  • 消费者是否需要动作
  • 如果不兼容,迁移路径是什么
  • 从哪个时间点或版本边界开始生效

很多团队的问题不是没 spec,而是 spec 变更太安静。
实现上线了,消费者只能靠线上报错才知道接口已经不一样了。

API 契约之所以稳定,不是因为你用了 OpenAPI。
而是因为你把它当成了一个要发布、要审查、要变更管理的产品表面。

spec-first 最容易失败的几种方式

这几个坑非常常见。

1. spec 变成补文档动作

真正的实现决策在别的地方发生,最后再回来补 YAML。
这不叫 spec-first,这叫事后备案。

2. spec 写得太抽象

没有示例、没有边界、没有清晰错误语义,这种 spec 很难指导实现,也很难指导消费方。

3. 过度迷信代码生成

什么都想生成,最后没有人真正对业务语义负责。

4. 兼容性只存在于口头

大家都说“这不算 breaking”,但没人有明确规则,结果消费者被动遇到问题。

5. spec 不进 CI

没有 lint、没有 diff review、没有兼容性检查、没有契约测试,漂移几乎是必然的。

一个比较实用的落地流程

如果你想把 spec-first 做成真能用的工程流程,通常可以按这个顺序来:

  1. 先定义资源和交互语义
  2. 起 OpenAPI 3.1 草案
  3. 把错误结构和 example 补完整
  4. 拉产品、后端、消费者一起评审
  5. 做 lint 和 schema 校验
  6. 生成 mock 工具链
  7. 按评审后的契约开始实现
  8. 加 provider / consumer 契约检查
  9. 对契约变更做版本和 changelog 管理

这套流程一点也不花哨,但它很有效。
它决定了你的 API 是“有一份文档”,还是“有一份可靠契约”。

最后一句

规范优先真正有价值,不是因为它形式更完整,而是因为它能把高成本错误往前挪。

如果你把 OpenAPI 3.1 当成:

  • 设计评审载体
  • mock 来源
  • 校验目标
  • 兼容性边界
  • 发布变更面

那 spec-first 很值得做。

如果你只是等服务写完,再导一份文档出来,那你做的不是规范优先,只是把历史补写了一遍。

而历史,往往比草案更难改。