tRPC 端到端类型安全实践(一):用 initTRPC 定义 Router 与 Procedure 📅 发布时间:2026/9/9 19:57:18 👁 浏览次数: tRPC 端到端类型安全实践一用 initTRPC 定义 Router 与 Procedure【免费下载链接】trpc♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc本篇文章以 tRPC 仓库 landing 页教学代码 Step1.md 为骨架系统讲解在 tRPC 中如何初始化根对象、用.input()接入 Zod 校验、编写.query()查询 procedure并导出供前端使用的AppRouter类型。读完你将掌握 tRPC 后端最小可用的完整写法理解initTRPC.create()、router、publicProcedure之间的源码级关系并能立刻在仓库 examples/minimal 或自己的项目中复制落地。Step1 在官方教程三步走中的位置在 tRPC 官网落地页的三步上手区块由 QuickIntro.tsx 渲染中Step1Step3 构成一条完整的端到端链路Define your procedures先定义 procedure本篇文章核心对应 Step1.mdCreate your HTTP server用createHTTPServer把 router 暴露成 HTTP 服务对应 Step2.mdConnect your client and start querying客户端传入AppRouter类型后获得类型推导与自动补全对应 Step3.md。procedure 是构建后端的原子单元它可组合可以是 query、mutation 或 subscription而 router 则用于聚合多个 procedure。Step1.md 给出的是最小但五脏俱全的例子我们先逐行拆解它再深入源码理解背后机制。逐行拆解 Step1 的核心代码import { initTRPC } from trpc/server; import z from zod; const t initTRPC.create(); const router t.router; const publicProcedure t.procedure; const appRouter router({ greeting: publicProcedure .input(z.object({ name: z.string() })) .query((opts) { const { input } opts; // ^? return Hello ${input.name} as const; }), }); export type AppRouter typeof appRouter;1.initTRPC.create()初始化后端根对象const t initTRPC.create();initTRPC是trpc/server导出的初始化入口。官方要求它在每个后端中只执行一次之后再从t上解构出复用的构建器而不是到处initTRPC.create()。从源码看这一约定是有依据的initTRPC本身是单例构建器create()会一次性把根配置RootConfig、procedure 构建器、middleware 工厂、router 工厂、mergeRouters、createCallerFactory全部组装好见 initTRPC.ts。create()还接受可选配置对象例如transformer、errorFormatter、isDev、allowOutsideOfServer、defaultMeta。其中默认isDev为process.env.NODE_ENV ! production更重要的是若在不支持服务器环境的地方调用create()且未显式开启allowOutsideOfServer: true源码会直接抛错以拦截误用见 initTRPC.ts。2. 导出可复用的router与publicProcedureconst router t.router; const publicProcedure t.procedure;这只是简单的别名赋值却是一种重要的工程约定把「初始化」与「业务定义」解耦。仓库推荐的工程习惯是把这两行放进独立的trpc.ts让所有子路由 import 它从而避免循环依赖。仓库示例 examples/minimal/src/server/trpc.ts 就是标准模板它还演示了在create()时传入transformer的写法。3.router({...})用对象聚合 procedureconst appRouter router({ greeting: publicProcedure /* ... */, });router 的键就是 procedure 的路径名值可以是单个 procedure也可以是嵌套的子 router 对象例如user: { list, byId, create }。examples/minimal/src/server/index.ts 展示了嵌套写法user.list、user.byId、user.create。procedure 类型层面的所有信息——输入解析器、输出类型、错误 shape——都会沿 router 结构被精确推导出来。4..input(z.object({ name: z.string() }))接入输入校验.input(z.object({ name: z.string() })).input()接收一个输入解析器。任何实现了对应接口的校验库都能接入。仓库源码 parser.ts 明确列出了这些形态校验库 / 形态源码识别方式说明zod.parse/.parseAsync最常用_input/_output可区分收窄前后类型valibot.schema/.parse兼容多种版本形态yup.validateSyncSchema 校验superstruct.createSchema 校验arktype.assert函数形式 schema需走assert而非直接调用Standard Schema~standard属性通用标准 schema 接口如 effect 等库自定义函数(input: unknown) TInput手写校验函数例如typeof val stringgetParseFn见 parser.ts会在运行时按上述顺序探测解析函数。因此你可以用 zod也可以换 yup、superstruct 或纯手写校验函数不依赖任何特定库。为什么使用校验库而不仅是 TS 类型因为 HTTP 到达的数据在类型层面是unknownTS 类型在编译后被擦除无法做运行时防御。.input()的解析器在运行时完成「校验 转型」并把结果类型精确注入到 resolver 的opts.input中。这就是一次定义、运行时与类型层同时生效的关键。5..query((opts) {...})写真正的业务逻辑.query((opts) { const { input } opts; return Hello ${input.name} as const; })query 的 resolver 接收一个参数对象opts其类型由 procedure 构建链上的上下文与输入解析器共同决定。从源码 procedureBuilder.ts 可以看到opts的标准字段ctx上下文、input经解析器校验后的输入、signal请求的 AbortSignal、pathprocedure 路径批处理场景下还有batchIndex。Step1 代码中的// ^?是文档 twoslash 类型标注展示编辑器里input已被精确推断为{ name: string }。除了.query()procedure 还可调用.mutation()写操作、HTTP POST与.subscription()订阅实时流三者共享同一构建链上的输入校验与中间件能力。本步只演示 query 即可覆盖最小闭环。6.export type AppRouter typeof appRouter把类型桥接给前端export type AppRouter typeof appRouter;这行导出是整个 tRPC 类型体系的灵魂。它只导出类型、不导出实现——client 端使用import type { AppRouter } from ./server即可import type会在编译期被完全擦除不产生任何运行时代码见 Step3.md 与 quickstart.mdx。前端拿到的只是 router 的类型签名实现细节完全封装在服务端不会泄漏。源码视角create() 之后我们拿到了什么initTRPC.create()返回的根对象上挂着 5 个核心构建入口见 initTRPC.ts 与接口定义 initTRPC.tsprocedureprocedure 构建器是.input()/.query()/.mutation()/.use()链式调用的起点router创建 router 的工厂把 procedure 或子 router 聚合为类型树middleware创建可复用中间件的工厂mergeRouters把多个 router 合并成一个常用于拆分业务模块createCallerFactory创建服务端直接调用器用于在服务端无 HTTP 地调用 procedure测试/SSR 常用。当调用.input()、.query()时实际上是在构建一个不可变的类型链每次调用都返回新的构建器把输入解析器、resolver 等信息累积进内部定义ProcedureBuilderDef见 procedureBuilder.ts。类型层则借助UnsetMarker区分尚未设置输入与已设置输入的状态从而在未设置.input()时把 resolver 的input推断为undefined避免误用。因此可以推断你看到的一切类型安全都发生在编译期推导 运行时解析器执行两个层面二者由同一段构建代码串联不会出现类型与行为不一致。让 Step1 跑起来接入 HTTP 服务器与客户端Step1.md 定位为三步流程的第一步配合仓库中同一目录的 Step2.md 与 Step3.md 即可形成可运行闭环// server/index.ts —— 对应 Step2 import { createHTTPServer } from trpc/server/adapters/standalone; import { appRouter } from ./appRouter; createHTTPServer({ router: appRouter }).listen(3000); // 服务监听在 3000 端口trpc/server/adapters/standalone是零依赖、基于 Node 原生http的适配器适合快速原型与本地联调。从源码 standalone.ts 可以看到它支持basePath选项默认/可设为/trpc/等请求路径会去掉basePath前缀后交给统一的nodeHTTPRequestHandler处理。更复杂的场景可替换为 Express、Fastify、Next.js、AWS Lambda 等适配器参见 adapters-intro.md 与 standalone.md。客户端侧对应 Step3.mdimport { createTRPCClient, httpBatchLink } from trpc/client; import type { AppRouter } from ./server; // type-only 导入编译期擦除 const trpc createTRPCClientAppRouter({ links: [ httpBatchLink({ url: http://localhost:3000 }), ], }); const res await trpc.greeting.query({ name: John }); // res 的类型被自动推断为 Hello JohncreateTRPCClientAppRouter只接收类型参数把AppRouter的类型签名注入客户端对象。httpBatchLink会把同一时间段内发起的多次调用合并成一次 HTTP 请求批量发送其内部 DataLoader 支持maxURLLength、maxItems等分割参数见 httpBatchLink.ts 的默认值两者默认均为Infinity即不做拆分。完整可运行的端到端示例可直接阅读仓库 examples/minimal其 index.ts 中定义了user.listquery、user.byId带z.string()输入的 query、user.create带对象输入的 mutation以及一个异步迭代器 query与 Step1 的模式完全一致。工程化落地清单把 Step1.md 的模式应用到自己项目时官方推荐按如下文件组织详见 quickstart.mdx. ├── server/ │ ├── trpc.ts # initTRPC.create() 导出 router / publicProcedure │ ├── appRouter.ts # 业务 procedure 定义 export type AppRouter │ └── index.ts # HTTP 服务器启动 └── client/ └── index.ts # createTRPCClientAppRouter 调用需要留意的硬性前提initTRPC.create()只执行一次且应放在独立模块中版本要求tRPC 要求 TypeScript 版本满足要求当前仓库官方文档标注 TypeScript 5.7.2并强烈建议开启strict: true见 quickstart.mdx 的 Requirements 说明子路由组织业务量大时可拆分为多个子 router再用t.mergeRouters合并输入校验任何自定义解析器都要保证校验失败必须抛错这样 procedure 才不会以错误输入继续执行。更多进阶内容可继续阅读仓库文档中与 Step1 直接相关的专题procedures.mdprocedure 全能力、validators.md输入解析器详解、routers.mdrouter 与初始化约定它们与 Step1.md 一脉相承可帮助你从能跑走向用得专业。【免费下载链接】trpc♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考