news 2026/9/9 23:27:26

使用 @trpc/openapi 从 tRPC Router 生成 OpenAPI 3.1 规范并打通任意语言客户端

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 @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/trpc

@trpc/openapi是 tRPC 官方生态中用于「无注解地从现有 tRPC Router 生成 OpenAPI 3.1 规范」的包:它静态分析你的 Router 的 TypeScript 类型,不执行任何业务代码,即可产出可供 Postman/Insomnia、任意语言代码生成器乃至 AI Agent(如 MCP 服务器)消费的openapi.json。读完本文,你将掌握 CLI 与编程式两种生成方式、spec 中 Procedure→HTTP 的映射规则,以及如何借助@trpc/openapi/heyapi生成类型安全的跨生态客户端并正确对齐 transformer(superjson / 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.ts(Hey API 客户端运行时配置),另有 src/index.ts 统一对外导出。

快速开始:生成第一份 spec

方式一:CLI

最简形式只需传入 Router 文件路径:

pnpm exec trpc-openapi ./src/server/router.ts

CLI 由 packages/openapi/src/cli.ts 基于 Node 内置parseArgs实现,支持的参数完整说明如下:

选项默认值说明
-e, --export <name>AppRouter文件导出的 Router 符号名(类型或值均可)
-o, --output <file>openapi.json输出文件路径(会自动创建父目录)
--title <text>tRPC API写入 OpenAPIinfo.title
--version <ver>0.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/trpc

cli.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.ts):

import { 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.ts:exportName(默认'AppRouter')、titleversionservers。其中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实现中可以逐段印证:

  1. 读取编译配置loadCompilerOptions从 Router 文件所在目录向上查找tsconfig.json并解析(找不到时回退到一组默认编译选项),再手动补齐moduleResolution推断(Node16/NodeNext、Preserve/ES2022/ESNext、Node10 分别映射到对应模式),见 packages/openapi/src/generate.ts。
  2. 构建 TS Program:用ts.createProgram建立以 Router 文件为入口的完整类型系统,然后通过getTypeChecker()拿到类型检查器。
  3. 定位并解析导出符号:同时支持「值导出」和export type别名两种情况——优先取valueDeclaration的类型,其次取getDeclaredTypeOfSymbol
  4. 递归遍历 RouterwalkType/walkRecord顺着 router 对象与子路由逐层展开(packages/openapi/src/generate.ts):遇到带_def的类型就检查其 procedure 类型;是query/mutation就提取 schema,是subscription则直接跳过(见下文);_def.router === true则继续下沉到子记录。
  5. 类型 → JSON Schema:用类型检查器把 input/output 的 TS 类型递归转换成 JSON Schema,命名类型自动注册进components/schemas并以$ref复用,从而正确处理递归与共享引用。
  6. 叠加运行时描述:在静态分析之外,还会尝试tryImportRouter动态导入 router 以收集collectRuntimeDescriptions(见schemaExtraction),把 Zod.describe()得到的字段描述叠加进 schema。
  7. 组装文档:最后构建 OpenAPI 3.1.1 文档对象。

由于核心是「读类型」,因此它天然具有两项特性:其一,无需为生成 spec 编写.output()schema,返回类型会从你的实现中自动推断;其二,执行分析的文件理论上不要求可运行(编译期即可完成绝大部分工作,动态导入失败只会让描述字段缺失而不会让整个流程崩溃)。

Procedure → HTTP 的映射规则(生成的 spec 长什么样)

路径与 HTTP 方法

  • query →GET /procedure.path,子路由用点号连接,例如user.listGET /user.list
  • mutation →POST /procedure.path,例如user.createPOST /user.create
  • subscription 被忽略(SSE 支持尚未实现)。

该映射由 packages/openapi/src/generate.ts 的buildOpenAPIDocument完成:procedure 完整路径(子路由以.拼接)作为 operationId 与 path,proc.path的第一个分段被用作 OpenAPI tag。每条 operation 的响应固定声明200成功响应 +default错误响应(引用#/components/responses/Error)。

输入参数编码

  • GET 的输入以input查询参数承载(整体为 JSON 字符串而非散开的字段),required: true,并带有style: deepObject(packages/openapi/src/generate.ts)。源码注释明确指出该style是 Hey API 正确生成查询序列化器的依赖;
  • POST 的输入作为application/jsonrequestBody(packages/openapi/src/generate.ts)。

换句话说,手工用 HTTP 工具调用user.byId(input 为字符串)时,请求形态是GET /user.byId?input=%22<encoded-id>%22

响应封装:tRPC Envelope

tRPC 的 HTTP 响应总是信封格式{ result: { data: T } },生成器据此把输出 schema 包进wrapInSuccessEnvelope(packages/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' }Promise<T>自动解包(对应 packages/openapi/src/generate.ts 的convertWellKnownType与字面量转换逻辑);
  • 枚举与字面量折叠'FOO' | 'BAR'这类字符串字面量联合会被折叠为{ type: 'string', enum: [...] }true | false折叠回boolean,TSenum同样支持;
  • union / intersection:可识别判别联合(所有成员共享同一带const值的必填属性)并为其补上discriminator;纯类型成员联合折叠成type数组;对象交集在无属性冲突时合并为一个对象 schema,否则退化为allOf
  • branded 类型透明string & { __brand: 'X' }这种打标类型在生成 schema 时会被剥离品牌标记、保留基础类型(unwrapBrand);
  • 递归与命名类型TreeNodeLinkedListNode这类递归 interface、z.lazy递归 schema、以及深层的命名 interface(UserProfileAddress等)都会注册成components/schemas中的命名 schema 并被多处$ref复用与去重(见 appRouter.router.ts 中namedTypes/recursiveTypes测试块);
  • 可选/可空/默认值语义:可选属性不进requirednullish/nullable生成type: [...]联合、.default()保持基础类型、refine/pipe/catch/passthrough/strict 均保留各自基础形态;
  • 数组、元组、Record:元组映射为prefixItems+items: false+ 长度约束;纯索引签名对象映射为additionalProperties;嵌套数组与深层对象照常递归。

适配现有 tRPC 服务的几个关键注意事项

官方「Adapting your tRPC setup」一节(www/docs/client/openapi.md)指出,生成器可直接作用于你已有的 router,无需任何注解或装饰器,但以下几点务必知晓:

  1. 输出类型可选:与其他 OpenAPI 工具不同,.output()schema 不是必需的——生成器自动从实现推断返回类型。
  2. transformer 必须两端对齐:若服务端启用了 data transformer,所有 OpenAPI 客户端必须使用同一个 transformer(详见下节)。
  3. subscription 暂被排除shouldIncludeProcedureInOpenAPI中只有type !== 'subscription'的 procedure 才会进入文档(packages/openapi/src/generate.ts),且是静默跳过,不会报错。
  4. 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,必须在代码生成阶段运行时各做一件事,否则DateMapSetBigInt等非 JSON 类型会在运行时静默出错(到达的是序列化后的原始对象而非原生类型)。

生成阶段:传入 type resolvers,让生成的类型(而不是运行时)就是正确的。这会要求你改用 Hey API 的编程式 API:

import { 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):对 schemaformatdate/date-time的 string 生成Date类型,对formatbigint的 number 生成bigint类型。遗漏它时,Hey API 只会把这些字段生成为string

运行时:给 client 配置与服务器一致的 transformer

import { 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)。以下是官方验证过的几种方案:

  • superjson(TS↔TS 场景最常用):支持DateMapSetBigIntRegExp等。安装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.ts(superjson)以及对应 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 请求的输入必须整体编码为?input=<JSON>而非拆成散开的查询参数。

把 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.md):

  • SSE 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 mistakes):packages/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/intent@latest 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:27:19

JavaWeb毕业设计实战:湿地公园旅游信息管理系统设计与实现全解析

刚开始带毕设那几年&#xff0c;我几乎每隔一段时间就会被问同一个问题&#xff1a;“老师/学长&#xff0c;JavaWeb的毕业设计到底做什么题比较好&#xff1f;”问的人多了&#xff0c;我发现大家真正焦虑的并不是技术&#xff0c;而是怕选一个“看起来像作业、答辩容易被挑刺…

作者头像 李华
网站建设 2026/9/9 23:22:58

PR曲线与ROC曲线:不平衡分类模型评估的终极指南

我做机器学习这几年&#xff0c;发现一个特别有意思的现象&#xff1a;很多人选模型的时候特别较真&#xff0c;XGBoost还是LightGBM、核函数用RBF还是多项式&#xff0c;研究得头头是道&#xff1b;但一到了模型评估环节&#xff0c;就只盯着准确率&#xff08;Accuracy&#…

作者头像 李华