跳过主要内容
开发 移动

OpenAPI TypeScript: 生成类型、客户端和验证

学习 OpenAPI TypeScript 生成的整个流程。生成类型、编写客户端、在运行时验证并从 CI 安全地部署。

OpenAPI TypeScript: 生成类型、客户端和验证

You can usually spot the moment an API pipeline starts lying to the team. A schema changes, the generated types update without complaints, the PR goes green, and then somebody in the front end keeps reading the old response shape because the wrapper cast the error away. That’s the OpenAPI TypeScript problem, not whether a generator can spit out interfaces.

有用的问题更难。您想要的 schema、传输和验证之间的哪个合同,以及哪些部分应该在构建时间失败而不是泄露到运行时?一旦您将其框定为 OpenAPI TypeScript 作为管道选择,权衡利弊变得更加清晰,工具不再试图成为整个解决方案。

目录

为什么生成的类型与安全的API 不一样

一位同事合并了一个PR,添加了一个可选的响应字段。生成的文件更新干净,diff看起来很无聊,大家都继续前进。然后前端继续读取一个旧的形状,通过一个手写的包装器进行“暂时”的 cast as any然后生产环境就像契约从未改变一样

那就是生成类型的陷阱 TypeScript 只能保护消费生成类型的code,并且只有当传输层不再擦除契约时才会如此。OpenAPI 端给你一个schema,而不是保证每个调用者都会尊重它。关于理解__CAPGO_KEEP_0__连接的讨论 discussion around understanding API connections 因为它推动了对系统连接的讨论,而不是单一工具的讨论,所以它在这里很有用。

失败的隐患在哪里。

最常见的断点是乏味的,而不是奇特的。 模式漂移 模式漂移发生在OpenAPI规范和部署的服务不再匹配时。 部分覆盖 部分覆盖会出现当规范只模型了happy path时,而应用程序依赖于未被文档化的边缘案例时。 手写的包装器 通常是类型被弱化的地方,尤其是当有人想要“快速行动”并使用 any 或松散响应 cast 时。

实用规则: 如果包装器可以撒谎,生成器就无法救你。

有一个运行时的差距。类型脚本类型在编译后会消失,所以它们无法拒绝来自网络的错误的 JSON。网络并不关心您的编辑器推断的内容,而这是为什么生成的客户端只是一层更安全的API pipeline中的一个层。

更广泛的运营问题是安全性和合同纪律,而不是仅仅是开发者方便。要了解API合同如何在更大的应用程序生命周期中放置,这个内部指南 API应用商店遵守性安全标准 是一个有用的伴侣。

成熟的方式来思考 openapi typescript 是这样的。它给你一个严格的schema-to-types桥梁,这很好,但它并不验证请求、强制运行时负载形状或阻止一个懒散的包装器破坏一切。生成器是容易的20%。剩下的就是pipeline设计,这是团队要么获得信任,要么积累错误信心的地方。

从OpenAPI Spec生成类型脚本类型

截图来自https://openapi-ts.dev

最轻便的有用设置通常是能经受住真实的仓库冲洗的设置。将OpenAPI spec放在同一个仓库中,生成一个提交的类型文件,并在CI中使漂移可见,而不是依赖于有人记住刷新步骤。一个命令 npx openapi-typescript ./openapi.yaml -o ./src/types/api.d.ts 给你一个可预测的输出文件,审阅者可以像检查任何其他源代码变化一样检查它。

实际上很重要的标志

The -o 输出标志很重要,因为它使生成的工件明确。 --immutable 有用时你想生成的类型保留 readonly 意图在输出中,并且 --alphabetize 当模式顺序改变而没有语义含义时,保持 diff 稳定。 --enum 在生成表面上,重要的是你的团队喜欢枚举而不是联合。

该项目的文档很清楚关于范围,它是一个 类型生成器,而不是客户端运行时或请求层, 并且这种限制有助于你想要轻量级、类型优先的设置。它的仓库也显示了工具背后的维护模型,这是为什么开源工具可以在生产中保持稳定,当文档和发布保持活跃时, GitHub repository and CLI documentation.

。关于这种立场的实用读物是项目的 package.json 所以命令位于构建脚本旁边,随着规范的变化而运行。在CI中,重新生成文件并在发生漂移时失败。 git diff 漂移的显示会让合同变更成为可见的审查工作,而不是静默的运行风险。

schema侧面同样重要,命令行。项目建议 compilerOptions.noUncheckedIndexedAccess 成为 additionalProperties ,这会在调用位置强制更安全的索引。它还建议使用 T | undefined而不是与额外的组合混用,并且在位置不明确时将 oneOf 放在根目录下,因为错误的定义可能会从生成的输出中消失。另外一个细节会节省时间 $defs 永远不会产生 openapi-typescript ,所以缺失的schema细节会提前暴露,而不是被宽松类型掩盖。 any保持规范明确,或者生成器会忠实地将模糊性暴露给你。

持续的工作流程很简单。将规范放在版本控制下,生成文件时重新生成,提交生成的文件,让类型检查器在任何人合并不匹配之前抱怨。这给了你整个管道的稳定合同边界。

__CAPGO_KEEP_0__

选择 Pure 类型、全客户端和无代码生成

模式 构建时间 输出文件 捆绑体重 最佳匹配
原生类型与薄包装 快速 希望控制并且有小运行时表面的小组
全客户端代码生成 较慢 众多 更高 希望快速交接和自动生成操作的团队
无需代码生成请求构建器 快速 无或最小 只有一份代码库的应用,偏好手写传输逻辑

选择不是“哪个工具获胜”。而是哪种pipeline形状适合你的仓库,团队,以及API看到的变动程度。 在2025年的一项benchmark中,围绕一个大约75,000行,2MB,约1,200个操作的OpenAPI规范 生成的输出大约在, openapi-typescript 几秒钟内 1.5秒 与大约 8.0秒 对于 @hey-api/openapi-ts, 5.5秒 对于 Orval, 并且 18.1秒 对于 Kubb, 与此同时产生一个单独的输出文件 16 对于 hey-api, 2,719 对于 Orval,和 3,877 用于 Kubb (benchmark 详情).

纯类型优先控制

纯类型的设置与手写请求层配对很好,因为您可以保持运行时小且API表面平淡。这在捆绑敏感的前端和在应用程序中很重要,一个团队拥有规范和消费者。您需要一个提醒,开发者体验不仅仅是语法糖时, 开发者体验角度 更容易判断您的客户code短、明显和可审查。

全客户端优先手动传输

openapi-generator, hey-api, Orval,和 Kubb 所有尝试做更多类型。那样有助于您想要请求方法、模型和管道一起生成,尤其是在后端和前端团队之间的大传输中。成本在benchmark上很明显,更多的生成文件、更多的运行时表面和更多的规范生长的构建摩擦。

无代码生成优先本地重构

类型化请求构建器和 fetch 当一个代码库同时拥有这两端的形状,并且API变化紧密协调时,wrapper就能发挥作用。然而,这也意味着需要维护良好的纪律。随着团队和仓库数量的增加,生产者和消费者之间的距离越来越远,手写的请求层会越来越容易脱离同步,除非您积极地强制执行契约测试。

核心决策点并非是意识形态问题。如果您的打包预算紧张,纯类型会很有吸引力。如果您的团队想要最大限度地减少设置时间,并且能够承受输出,完整客户端会是最佳选择。如果您想要最小化移动部分,并且能够保持契约接近,零代码生成请求构建器可能是内部权衡的正确选择。

使用 Fetch 或 Axios 构建 Thin Typed Client

使用生成的 TypeScript API 定义为 web 请求的 Typed Client Wrapper 过程的示意图。

薄层包装是生成器停止的地方,应用程序code开始的地方。包装层应该暴露一个函数,接受类型化的参数和查询对象,并将调用转发给 fetch 或注入 axios 的实例,而不试图变得聪明。在大多数生产环境中,这层代码通常不会超过30-60行,因为生成的类型已经包含了大部分的形状。 这里的思维模型是有效的: 路径参数保持类型化

所以

  • __CAPGO_KEEP_0__ __CAPGO_KEEP_0__ /users/{id} 不能不带参数调用 id.
  • Query 对象保持类型 所以可选过滤器不会变成字符串汤。
  • 响应体保持类型 所以解析 code 可以信任它期望的窄形。

像这样的包装器是故意无聊的。它不应该在其他地方属于的重试、转换或认证策略中发明它们。它应该将类型化的操作从类型化的结果中移动到传输层,然后将类型化的结果返回到上层。

保持包装器无聊且依赖轻量,否则每次代码生成变化都会波及你的应用。

常见的失败是通过 as any 来修补不匹配的缺陷,当生成的类型与旧包装器签名不符时。这样可以获得绿色构建并且应用脆弱。它还隐藏了你希望生成器揭示的契约破坏。

对于喜欢使用 Axios 的团队,模式是相同的,只是传输实现发生了变化。对于想要更简单的浏览器端 code fetch 通常就足够了。重要的是请求函数接受生成的路径类型并返回类型化的响应,而不是后来进行大量操作的松散对象。

如果你使用这个接口很好地, openapi typescript 给你一个清晰的劳动力分配。Schema存放在规范中,传输存放在包装器中,应用程序看到类型化的操作而不是 ad hoc 请求 code。

Adding Runtime Validation with zod, ajv, or io-ts

TypeScript类型在运行时消失,网络不关心编辑器的信心。因此,安全模式不是“生成类型并希望”,而是“生成类型,然后在边界处验证未经信任的数据进入应用”。生成的schema仍然是真实的来源,验证库,如 zod, ajv, 和 io-ts 处理编译时类型无法编译的边界检查。

Validate where the data enters

对于React应用程序,边界通常是在请求解析后,payload进入状态之前。对于服务器,边界是在payload写入数据库或交付给业务规则之前。规则很简单,保持验证靠近边界,不要散布手动检查到特性 code。

A zod shape可以反映生成的响应形状而不替换它:

import { z } from "zod";

const WeatherForecastSchema = z.object({
  date: z.string(),
  temperatureC: z.number(),
  summary: z.string().nullable(),
  temperatureF: z.number().optional(),
});

该示例验证schema标记为可选的字段,并且它保持了运行时检查与生成器产生的对齐。 ajv 是选择高吞吐量JSON Schema验证的强劲选择,适用于服务器 io-ts 仍然适合已经生活在 fp-ts 组成风格的团队。

验证太晚是大错误。如果载荷先进入您的应用程序,类型系统已经被绕过,错误有地方躲藏。关于 JavaScript单元测试 的简短指南,和这个思维方式非常匹配,因为单元测试和边界验证都在捕捉早期错误的坏假设时效果最佳。

清晰的层次结构是可预测的。 OpenAPI TypeScript generates the contract, the validator checks the runtime payload, and your app code only sees data that survived both steps. That’s a much better boundary than trusting a static type to police an untrusted response.

将生成、验证和契约测试添加到CI中

截图来自https://github.com

一个持续的管道将契约转化为门槛,而不是建议。重新生成类型,失败于漂移,运行 tsc --noEmit,并在合并之前将API形状与模拟或契约工具进行测试。如果您将生成器版本固定在 package.json, 两个工程师无法意外地从同一规范中产生不同的输出。

一个简单的 GitHub Actions 形式

一个实际的工作流程如下:

  1. 从仓库或生成的源代码中拉取规范。
  2. 重新生成类型。
  3. 如果发生变化 git diff 显示变化
  4. 执行一个合同测试 tsc --noEmit.
  5. 与一个像 Prism 或 Spectral 支持的检查一样的 mock 服务器

合同测试和快照测试之间的关键区别是范围。快照通常告诉你文件改变了。合同测试告诉你形状是否仍然像规范说它应该那样行为。

A mock server is especially useful when backend and frontend work are separated by time or team boundaries. It gives consumer code a predictable API surface while still checking the actual contract rather than a hard-coded fixture. The 持续集成设置指南 如果您的团队仍然需要一个干净、可重复的CI基线,那么这是一个有用的参考。

通过固定生成器版本,可以避免代码生成管道中的一个最糟糕的故障:不可见的输出偏差。如果一个开发者在本地升级了生成器,而另一个没有,那么生成的文件就可能变成随机噪声而不是信号。CI应该使这种情况不可能发生。

结果是一个管道,其中schema变化、类型生成、编译器检查和契约测试都相互强化。这就是使工作流程诚实的原因。

可维护的管道、性能和最后的检查清单

一个清单,展示了四个关键步骤,用于维护OpenAPI TypeScript管道以支持软件开发项目。

能够存活的管道是那些具有平凡的治理的管道。版本化规范,审查schema变化,如code,固定生成器,记录破坏性变化的批准过程。如果过程模糊,人们会绕过它,生成的类型就变成了装饰而不是强制执行。

几个性能杠杆实际上很重要

增量生成有助于在单元仓库中,规范变化频繁,但只有一个包消费它。 tsc --incremental 可以去掉重复的编译工作,禁用生产构建中不需要的输出标志,可以让生成的表面更小。在实践中,最大收益仍然是社会性的,而不是技术性的,因为可预测的管道比聪明的管道更常被运行。

以下清单是值得保留的清单:

  • 版本固定: 锁定 openapi-typescript 版本在 package.json 因此输出不会在机器之间漂移。
  • Schema 检查: 将规范变更视为可审查的合同变更,而不是清洁工作。
  • 漂移检测: 在 CI 中重新生成并在 diff 失败时失败。
  • 边缘验证: 在应用状态或持久性之前解析未受信任的负载。
  • 合同测试: 运行一个 mock-back 的检查,证明消费者 code仍然与 schema 匹配。
  • 破坏性变更政策: 写下谁批准形状变更以及如何通知客户。

一条包含这些门控的管道不仅会生成类型,它还会使契约可见。这种可见性是使团队不信任仅看起来安全的文件的原因。

如果您正在部署Capacitor或Electron应用,并希望您的更新管道表现出同样的纪律,Capgo为您提供了一种实用的方法来快速移动JavaScript、CSS、复制、配置和资产修复,而无需等待应用商店审查。访问 Capgo 查看其已签名的捆绑包、回滚保护和发布控制如何适应需要速度但不失控的发布过程。

实时更新Capacitor应用

当一个web层bug活跃时,通过Capgo将修复推送到用户,而不是等待几天的应用商店审批。用户在后台接收更新,而原生变化仍在正常审批路径中。

来自马丁的人性化支持

立即开始

最新博客

Capgo为您提供了创建真正专业的移动应用所需的最佳见解。