使用 @trpc/openapi 从 tRPC Router 生成 OpenAPI 3.1 规范并打通任意语言客户端
使用 trpc/openapi 从 tRPC Router 生成 OpenAPI 3.1 规范并打通任意语言客户端【免费下载链接】trpc♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpctrpc/openapi是 tRPC 官方生态中用于「无注解地从现有 tRPC Router 生成 OpenAPI 3.1 规范」的包它静态分析你的 Router 的 TypeScript 类型不执行任何业务代码即可产出可供 Postman/Insomnia、任意语言代码生成器乃至 AI Agent如 MCP 服务器消费的openapi.json。读完本文你将掌握 CLI 与编程式两种生成方式、spec 中 Procedure→HTTP 的映射规则以及如何借助trpc/openapi/heyapi生成类型安全的跨生态客户端并正确对齐 transformersuperjson / EJSON 等。包概览它解决什么问题在一个纯 tRPC 项目中客户端与服务端类型天然一致但一旦需要跨出 TypeScript 生态第三方语言客户端、HTTP 调试工具、AI 工具链就需要一份标准化的接口描述。该包生成的 OpenAPI 3.1 文档可以用于在任意语言中生成类型化的 API 客户端通过 Postman、Insomnia 等 HTTP 工具直接调用 tRPC 端点为可消费 OpenAPI 的 AI Agent 集成如 MCP server提供接口契约。官方文档在 packages/openapi/README.md 中以「OpenAPI schema generation for tRPC」为标题将其定位为「从你的 tRPC router 生成 OpenAPI 3.1 规范」。需要说明的是该包在仓库中版本号为11.18.0-alpha见 packages/openapi/package.json官方明确标注 alpha 状态、API 可能无通知变更建议与你在用的 tRPC v11 版本号对齐使用。安装与包结构在任意同时依赖trpc/server的项目中安装pnpm add trpc/openapi安装后即获得两个可执行入口与两个导出入口全部由 packages/openapi/package.json 声明bin字段暴露了trpc-openapi映射到dist/cli.js这就是 CLI 的核心命令包的exports暴露主入口.提供generateOpenAPIDocument与类型和./heyapi提供 Hey API 客户端集成所需的辅助函数。从源码目录看该包内部划分为三个核心文件src/cli.ts命令行参数解析与文件读写、src/generate.ts类型遍历与文档构建、src/heyapi/index.tsHey API 客户端运行时配置另有 src/index.ts 统一对外导出。快速开始生成第一份 spec方式一CLI最简形式只需传入 Router 文件路径pnpm exec trpc-openapi ./src/server/router.tsCLI 由 packages/openapi/src/cli.ts 基于 Node 内置parseArgs实现支持的参数完整说明如下选项默认值说明-e, --export nameAppRouter文件导出的 Router 符号名类型或值均可-o, --output fileopenapi.json输出文件路径会自动创建父目录--title texttRPC API写入 OpenAPIinfo.title--version ver0.0.0写入 OpenAPIinfo.version--server-url url无写入servers[].url需包含 tRPC 挂载前缀-h, --help-打印帮助并退出带完整参数的实际调用示例pnpm exec trpc-openapi ./src/server/router.ts -o api.json \ --title My API --version 1.0.0 \ --server-url https://api.example.com/trpccli.ts中实现细节值得注意packages/openapi/src/cli.ts参数校验失败未知选项、缺少router-file、文件不存在、导出符号找不到都会以非零码退出并输出可读的错误信息--server-url会在内部被构造成servers: [{ url: … }]传给生成函数最终产物用JSON.stringify(doc, null, 2) \n落盘。也就是说--server-url是 CLI 提供的编程式 API 中servers数组的便捷别名。方式二编程式需要把生成逻辑并入构建脚本、CI 或「先生成 spec 再喂给其他工具」的流水线时可直接调用导出函数packages/openapi/src/index.tsimport { generateOpenAPIDocument } from trpc/openapi; const doc await generateOpenAPIDocument(./src/server/router.ts, { exportName: AppRouter, title: My API, version: 1.0.0, servers: [{ url: https://api.example.com/trpc }], });生成选项GenerateOptions的完整定义位于 packages/openapi/src/generate.tsexportName默认AppRouter、title、version与servers。其中servers数组会原样透传到生成的文档中且源码注释特别强调每个url都应包含 tRPC 挂载前缀——因为生成出的路径如/user.create是相对于该前缀的例如服务挂在/trpc下就要写https://api.example.com/trpc省略servers时文档将不含该字段。两种方式的共同前提是文件必须真实导出你指定的 Router 符号。生成函数会先解析入口文件的所有导出找不到目标导出时抛出异常并列出可用导出列表packages/openapi/src/generate.ts。文档中推荐的「文件导出 Router 类型」写法与示例仓库一致见 examples/openapi-codegen/src/server/index.ts定义export const appRouter router({...})之后再export type AppRouter typeof appRouter;。工作原理静态类型分析绝不执行你的代码官方文档与 CLI 帮助中都强调一句话该生成器静态分析 Router 的 TypeScript 类型从不执行你的代码。这在 packages/openapi/src/generate.ts 的generateOpenAPIDocument实现中可以逐段印证读取编译配置loadCompilerOptions从 Router 文件所在目录向上查找tsconfig.json并解析找不到时回退到一组默认编译选项再手动补齐moduleResolution推断Node16/NodeNext、Preserve/ES2022/ESNext、Node10 分别映射到对应模式见 packages/openapi/src/generate.ts。构建 TS Program用ts.createProgram建立以 Router 文件为入口的完整类型系统然后通过getTypeChecker()拿到类型检查器。定位并解析导出符号同时支持「值导出」和export type别名两种情况——优先取valueDeclaration的类型其次取getDeclaredTypeOfSymbol。递归遍历 RouterwalkType/walkRecord顺着 router 对象与子路由逐层展开packages/openapi/src/generate.ts遇到带_def的类型就检查其 procedure 类型是query/mutation就提取 schema是subscription则直接跳过见下文_def.router true则继续下沉到子记录。类型 → JSON Schema用类型检查器把 input/output 的 TS 类型递归转换成 JSON Schema命名类型自动注册进components/schemas并以$ref复用从而正确处理递归与共享引用。叠加运行时描述在静态分析之外还会尝试tryImportRouter动态导入 router 以收集collectRuntimeDescriptions见schemaExtraction把 Zod.describe()得到的字段描述叠加进 schema。组装文档最后构建 OpenAPI 3.1.1 文档对象。由于核心是「读类型」因此它天然具有两项特性其一无需为生成 spec 编写.output()schema返回类型会从你的实现中自动推断其二执行分析的文件理论上不要求可运行编译期即可完成绝大部分工作动态导入失败只会让描述字段缺失而不会让整个流程崩溃。Procedure → HTTP 的映射规则生成的 spec 长什么样路径与 HTTP 方法query →GET /procedure.path子路由用点号连接例如user.list→GET /user.listmutation →POST /procedure.path例如user.create→POST /user.createsubscription 被忽略SSE 支持尚未实现。该映射由 packages/openapi/src/generate.ts 的buildOpenAPIDocument完成procedure 完整路径子路由以.拼接作为 operationId 与 pathproc.path的第一个分段被用作 OpenAPI tag。每条 operation 的响应固定声明200成功响应 default错误响应引用#/components/responses/Error。输入参数编码GET 的输入以input查询参数承载整体为 JSON 字符串而非散开的字段required: true并带有style: deepObjectpackages/openapi/src/generate.ts。源码注释明确指出该style是 Hey API 正确生成查询序列化器的依赖POST 的输入作为application/json的requestBodypackages/openapi/src/generate.ts。换句话说手工用 HTTP 工具调用user.byIdinput 为字符串时请求形态是GET /user.byId?input%22encoded-id%22。响应封装tRPC EnvelopetRPC 的 HTTP 响应总是信封格式{ result: { data: T } }生成器据此把输出 schema 包进wrapInSuccessEnvelopepackages/openapi/src/generate.ts有输出时为{ result: { data: T } }无输出时省略data属性。错误响应统一包成{ error: errorShape }其中 errorShape 优先从 Router 配置的_def._config.$types.errorShape提取取不到时回退到一个默认对象message/code/data等字段见 packages/openapi/src/generate.ts 与 packages/openapi/src/generate.ts。类型能力的覆盖面从 packages/openapi/test/routers/appRouter.router.ts 这个覆盖面极广的测试 Router 可以看到生成器对 TypeScript/Zod 类型系统的支持粒度原生类型映射Date→{ type: string, format: date-time }Uint8Array/Buffer→{ type: string, format: binary }bigint→{ type: integer, format: bigint }PromiseT自动解包对应 packages/openapi/src/generate.ts 的convertWellKnownType与字面量转换逻辑枚举与字面量折叠FOO | BAR这类字符串字面量联合会被折叠为{ type: string, enum: [...] }true | false折叠回booleanTSenum同样支持union / intersection可识别判别联合所有成员共享同一带const值的必填属性并为其补上discriminator纯类型成员联合折叠成type数组对象交集在无属性冲突时合并为一个对象 schema否则退化为allOfbranded 类型透明string { __brand: X }这种打标类型在生成 schema 时会被剥离品牌标记、保留基础类型unwrapBrand递归与命名类型TreeNode、LinkedListNode这类递归 interface、z.lazy递归 schema、以及深层的命名 interfaceUserProfile、Address等都会注册成components/schemas中的命名 schema 并被多处$ref复用与去重见 appRouter.router.ts 中namedTypes/recursiveTypes测试块可选/可空/默认值语义可选属性不进required、nullish/nullable生成type: [...]联合、.default()保持基础类型、refine/pipe/catch/passthrough/strict 均保留各自基础形态数组、元组、Record元组映射为prefixItemsitems: false 长度约束纯索引签名对象映射为additionalProperties嵌套数组与深层对象照常递归。适配现有 tRPC 服务的几个关键注意事项官方「Adapting your tRPC setup」一节www/docs/client/openapi.md指出生成器可直接作用于你已有的 router无需任何注解或装饰器但以下几点务必知晓输出类型可选与其他 OpenAPI 工具不同.output()schema 不是必需的——生成器自动从实现推断返回类型。transformer 必须两端对齐若服务端启用了 data transformer所有 OpenAPI 客户端必须使用同一个 transformer详见下节。subscription 暂被排除shouldIncludeProcedureInOpenAPI中只有type ! subscription的 procedure 才会进入文档packages/openapi/src/generate.ts且是静默跳过不会报错。description 自动采集Zod.describe()、以及类型/子路由/procedure 上的 JSDoc 注释都会成为 spec 中的description字段无需额外标注。这得益于 generate.ts 中 JSDoc 提取对node_modules内声明文件做了过滤以保留 monorepo workspace 链接包的注释与运行时 describe 叠加两层机制。客户端生成与trpc/openapi/heyapi桥接任何 OpenAPI 客户端生成器理论上都可消费该 spec但官方文档明确「测试最充分」的是与 Hey API 的集成。生成出的 SDK 与你的 procedure 一一对应queries→GET、mutations→POST、subscriptions 被忽略。无 transformer 的最简路径先安装生成器再用 Hey API 的 CLI 从 spec 生成客户端代码pnpm add trpc/openapi hey-api/openapi-ts pnpm exec openapi-ts -i openapi.json -o ./generated由于 tRPC 协议的特殊性信封结构、GET 输入编码方式生成出的裸客户端不能直接用需要做运行时桥接import { configureTRPCHeyApiClient } from trpc/openapi/heyapi; import { client } from ./generated/client.gen; import { Sdk } from ./generated/sdk.gen; configureTRPCHeyApiClient(client, { baseUrl: http://localhost:3000, }); const sdk new Sdk({ client }); // Queries - GET, Mutations - POST const result await sdk.greeting({ query: { input: { name: World } } }); const user await sdk.user.create({ body: { name: Bob, age: 30 } });注意调用形态query 的入参包在{ query: { input: … } }里mutation 的入参包在{ body: … }里返回数据则始终要通过信封取值result.data?.result.data才是 procedure 真正的返回值见 packages/openapi/skills/openapi/SKILL.md 的「Response shape」一节。configureTRPCHeyApiClient的内部实现在 packages/openapi/src/heyapi/index.ts它把三段配置一次性灌进 Hey API client ——querySerializer把 query 参数序列化为 URLSearchParams其中input键走JSON.stringify若配置了 transformer 则对input值先做transformer.input.serialize再编码bodySerializerJSON.stringify(transformer.input.serialize(body))仅在有 transformer 时注入responseTransformer识别信封中的result对其中的result.data调用transformer.output.deserialize还原富类型仅在有 transformer 时注入error interceptor有 transformer 时还会注册createTRPCErrorInterceptor对错误体中的error做同样的反序列化。有 transformer 时的两条关键配置如果服务端启用了数据 transformer必须在代码生成阶段和运行时各做一件事否则Date、Map、Set、BigInt等非 JSON 类型会在运行时静默出错到达的是序列化后的原始对象而非原生类型。生成阶段传入 type resolvers让生成的类型而不是运行时就是正确的。这会要求你改用 Hey API 的编程式 APIimport { createClient } from hey-api/openapi-ts; import { createTRPCHeyApiTypeResolvers } from trpc/openapi/heyapi; await createClient({ input: ./openapi.json, output: ./generated, plugins: [ { name: hey-api/typescript, // 关键确保生成的类型如 Date、bigint正确 ~resolvers: createTRPCHeyApiTypeResolvers(), }, { name: hey-api/sdk, operations: { strategy: single }, }, ], });createTRPCHeyApiTypeResolvers的实现非常直接packages/openapi/src/heyapi/index.ts对 schemaformat为date/date-time的 string 生成Date类型对format为bigint的 number 生成bigint类型。遗漏它时Hey API 只会把这些字段生成为string。运行时给 client 配置与服务器一致的 transformerimport { configureTRPCHeyApiClient } from trpc/openapi/heyapi; import superjson from superjson; import { client } from ./generated/client.gen; configureTRPCHeyApiClient(client, { baseUrl: http://localhost:3000, // 必须与服务端 transformer 一致 transformer: superjson, });配置完成后即可直接传原生类型并取回反序列化后的原生类型const sdk new Sdk({ client }); const event await sdk.getEvent({ query: { input: { id: evt_1, at: new Date(2025-06-15T10:00:00Z) } }, }); // event.data.result.data.at 是 Date 实例 ✅官方文档与 skill 都把「client 忘了配 transformer」列为头号常见错误典型症状是所有日期字段都变成序列化后的对象而非Date。Transformer 的跨生态选型tRPC 的 DataTransformer 接口就是{ serialize, deserialize }两个方法configureTRPCHeyApiClient接受任意实现该接口的对象或{ input, output }组合形式源码resolveTransformer会统一规范化见 packages/openapi/src/heyapi/index.ts。以下是官方验证过的几种方案superjsonTS↔TS 场景最常用支持Date、Map、Set、BigInt、RegExp等。安装superjson后服务端initTRPC.create({ transformer: superjson })客户端透传同一对象即可。MongoDB EJSON需要跨语言时bson包提供的EJSON.serialize/EJSON.deserialize与 tRPCDataTransformer一一对应官方资料中覆盖 C、Go、Java、Python、Ruby 等多种语言import { EJSON } from bson; import type { TRPCDataTransformer } from trpc/server; export const ejsonTransformer: TRPCDataTransformer { serialize: (value) EJSON.serialize(value), deserialize: (value) EJSON.deserialize(value as Document), };Amazon Ion不支持直接实现TRPCDataTransformer接口需要少量样板代码包装官方仓库提供了完整端到端实现与测试。自定义 transformer任何{ serialize, deserialize }对象都可用服务端传给initTRPC.create、客户端传给configureTRPCHeyApiClient。上述场景的完整端到端验证分别落在 packages/openapi/test/mongoEjson.test.ts、packages/openapi/test/amazonIon.test.ts、packages/openapi/test/generate.test.tssuperjson以及对应 fixture Router packages/openapi/test/routers/superjsonRouter.router.ts、packages/openapi/test/routers/mongoEjsonRouter.router.ts、packages/openapi/test/routers/amazonIonRouter.router.ts可作为移植参考。若不用 Hey API 而选其他生成器/语言要正确打通 tRPC 协议你的生成客户端必须满足两个硬性条件www/docs/client/openapi.md一是使用与服务端相同的 transformer 做输入序列化与输出反序列化二是 GET 请求的输入必须整体编码为?inputJSON而非拆成散开的查询参数。把 spec 生成接入日常工作流一个端到端参考示例仓库 examples/openapi-codegen/src 演示了完整闭环其结构是server/tRPC 服务与 router、shared/transformer.ts共享 transformer、scripts/codegen.ts脚本化两步先generateOpenAPIDocument产出 spec再调用 Hey APIcreateClient生成客户端、client/生成出的sdk.gen.ts/types.gen.ts与使用入口。脚本核心流程与 packages/openapi/skills/openapi/SKILL.md 中 codegen 脚本一致import { rmSync, writeFileSync } from node:fs; import { createClient } from hey-api/openapi-ts; import { generateOpenAPIDocument } from trpc/openapi; import { createTRPCHeyApiTypeResolvers } from trpc/openapi/heyapi; // 1. 从 router 生成 OpenAPI spec const doc await generateOpenAPIDocument(./src/server/index.ts, { exportName: appRouter, title: Example API, version: 1.0.0, }); writeFileSync(openapi.json, JSON.stringify(doc, null, 2) \n); // 2. 由 spec 生成类型安全的 Hey API 客户端 await createClient({ input: openapi.json, output: ./generated, plugins: [ { name: hey-api/typescript, ~resolvers: createTRPCHeyApiTypeResolvers() }, { name: hey-api/sdk, operations: { strategy: single } }, ], });官方文档还推荐了一个加分实践用oasdiff对比两个版本 spec 来做 changelog 与破坏性变更检查例如对比仓库测试 fixturepackages/openapi/test/routers/superjsonRouter.openapi.json与演进后的新 spec即可输出类似new-required-request-property新增必填请求属性、api-path-removed-without-deprecation路径未弃用即被移除这样的结构化变更信息帮助规划与协调 API 发布。已知限制与路线图README 中的 TODO 与 skill 中的「Common Mistakes」共同勾勒出当前边界均为仓库内的规划文本可查 packages/openapi/README.mdSSE subscriptions目前完全被静默排除在 spec 之外计划支持但尚未实现若你期望 spec 中出现 subscription请不要惊讶于缺失。非 JSON 内容类型可能已可用但缺少测试覆盖。async generator 支持生成类型效果不佳仍在调研中。其他规划非 Node.js 示例、AI/MCP 示例、跨生态对 transformer 需求的工作区部分选项已有文档、不带TrpcEnvelope的 REST 翻译层与 GET 的替代 query 参数格式等。易错点再强调一遍使用 transformer 却忘记在 Hey API client 中同步配置Date静默损坏、忘记createTRPCHeyApiTypeResolvers日期类型退化为string、以及导出名写错CLI 默认找AppRouter若文件导出的是appRouter值需显式-e appRouter找不到导出时错误信息会列出文件中所有可用导出名见 packages/openapi/src/generate.ts。延伸阅读仓库内面向 AI Agent 的技能文档含完整 setup/pattern/common mistakespackages/openapi/skills/openapi/SKILL.md最完整的使用文档www/docs/client/openapi.md生成器核心实现packages/openapi/src/generate.ts、CLI 实现packages/openapi/src/cli.ts、Hey API 桥接packages/openapi/src/heyapi/index.ts覆盖面广的测试 Router可当能力清单阅读packages/openapi/test/routers/appRouter.router.ts端到端示例工程examples/openapi-codegen/src包导出与 bin 声明packages/openapi/package.json如果你使用 AI 编码 Agent官方还建议通过npx tanstack/intentlatest install安装 tRPC skills以获得更好的代码生成质量。【免费下载链接】trpc♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考