tRPC 极简实战:用 Express + Vanilla TRPCClient 搭建端到端类型安全 API
2026/9/6 22:43:37 网站建设 项目流程

tRPC 极简实战:用 Express + Vanilla TRPCClient 搭建端到端类型安全 API

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

本篇以 tRPC 仓库中的 Express 最小示例为蓝本,演示如何用最少代码搭出一个完整的 tRPC 应用:一个挂载createExpressMiddleware的 Express 服务端,加一个运行在 Node 环境中的 VanillaTRPCClient。读完本文,你将掌握该示例的目录结构、启动方式、端到端类型推导是如何工作的,以及修改服务端程序名或输入时客户端为何会立即报类型错误。

示例包含什么

examples/express-minimal/README.md 明确说明该示例只包含两样东西:

  • Express server:基于 Express 5 的 HTTP 服务,通过 tRPC 官方 Express 适配器暴露/trpc路由;
  • Vanilla TRPCClient in Node:不使用 React、不使用浏览器,直接在 Node 进程里用@trpc/client发起类型安全的请求。

这是理解 tRPC 最底层的“去框架化”形态:不依赖@trpc/react-querynext等上层集成,只看 core 部分——Router、Procedure、Adapter、Link 四件套如何组合成一个可运行的 API。

运行方式

在 examples/express-minimal/package.json 中,各脚本定义如下:

# 同时启动 server 与 client(README 推荐入口) yarn start # 实际是 pnpm dev,即 run-p dev:* --print-label # 拆解开来: dev:server # tsx watch src/server —— 监听并运行服务端 dev:client # wait-port 3000 && tsx watch src/client —— 等端口就绪后运行客户端 # 生产形态:esbuild 打包后分别运行 build # esbuild src/server.ts src/client.ts --bundle ... --outdir=dist test-start # start-server-and-test 'node dist/server' 3000 'node dist/client'

几个值得注意的细节:

  • 客户端脚本用wait-port 3000等待服务端先监听 3000 端口,避免连接被拒;
  • test-dev/test-start使用start-server-and-test把“起服务、等端口、跑客户端”串成一个自动化流程,这是该示例被当作集成测试用例的入口;
  • 依赖面非常小:express@^5.0.0zod@^4.2.1加上@trpc/server@trpc/client@trpc/react-query(其中 react-query 在该示例里并未真正使用),运行时代码只有 server/router/client 三个文件。

服务端:Express 适配器

完整代码见 src/server.ts:

import { createExpressMiddleware } from '@trpc/server/adapters/express'; import express from 'express'; import { appRouter } from './router'; async function main() { const app = express(); // 供 wait-port / 健康检查使用的根路由 app.get('/', (_req, res) => { res.send('Server is running!'); }); app.use( '/trpc', createExpressMiddleware({ router: appRouter, }), ); app.listen(3000); } void main();

要点拆解:

  1. createExpressMiddleware({ router })返回一个标准 Express handler,挂载在/trpc前缀下,所有 tRPC 请求都以http://localhost:3000/trpc/<procedure>的形式到达;
  2. GET /路由只是为“等待端口就绪”提供的探测端点,与 tRPC 无关;
  3. 适配器本身极薄。从源码 packages/server/src/adapters/express.ts 看,createExpressMiddleware只是取出请求路径的最后一段作为 procedure path,然后委托给通用的nodeHTTPRequestHandler处理,异常统一交给internal_exceptionHandler。也就是说 Express 适配器与 standalone、fetch 等 Node 系适配器共享同一套 HTTP 处理内核,差异仅在如何读取req.path与如何挂到框架上。

官方文档中对应的适配器说明可参考 www/docs/server/adapters/express.md。

路由定义:嵌套 Router 与 nullish 输入

Router 见 src/router.ts,共 17 行,展示了 v11 的嵌套路由写法:

import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const publicProcedure = t.procedure; const router = t.router; export const appRouter = router({ hello: { greeting: publicProcedure .input(z.object({ name: z.string() }).nullish()) .query(({ input }) => { return `Hello ${input?.name ?? 'World'}`; }), }, }); export type AppRouter = typeof appRouter;

几个关键设计:

  • 嵌套 routerhello本身是一个子 router,greeting是其中的 query,因此完整 procedure 路径为hello.greeting,客户端按client.hello.greeting的点分路径访问;
  • nullish()输入z.object({ name: z.string() }).nullish()使 input 同时允许undefined/null{ name: string },服务端用input?.name ?? 'World'兜底,实现“可选参数”的端到端类型化;
  • AppRouter类型导出typeof appRouter是整套类型安全体系的枢纽——客户端导入这个类型后,所有 procedure 名、input、output 都由它推导。

客户端:Vanilla TRPCClient + httpBatchLink

Node 端客户端见 src/client.ts:

import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './router'; async function main() { const client = createTRPCClient<AppRouter>({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', }), ], }); try { const withoutInputQuery = await client.hello.greeting.query(); console.log(withoutInputQuery); const withInputQuery = await client.hello.greeting.query({ name: 'Alex' }); console.log(withInputQuery); } catch (error) { console.error('Error:', error); } } void main();

流程是:createTRPCClient<AppRouter>把路由类型“压入”客户端,links数组决定请求如何发出。这里只有一个httpBatchLink,指向服务端的/trpc挂载点。两次query()调用分别演示了“无 input”与“带 input”两种调用形态,返回值类型分别由 router 中的 handler 推导。

从 packages/client/src/links/httpBatchLink.ts 的实现看,batch link 内部按query/mutation各建了一个 Data Loader,同一 tick 内发起的多个请求会被合并为一次 HTTP 调用:procedure path 用逗号拼接进 URL、input 数组以 JSON 放入请求体,服务端逐个解析后按序返回结果数组;如果传入maxURLLength/maxItems(默认均为Infinity),超出阈值则自动拆批。此外该文件明确抛错提示httpBatchLink不支持 subscription,需要订阅时应改用httpSubscriptionLinkwsLink——这与本示例“纯 query”的定位正好吻合。

端到端类型安全:README 里的“试一试”

README 给出了一条最直观的验证建议(原文):

Tip: Try changing either the procedure name or it's input on the server and see the client type-erroring.

把这条建议展开,可以做一个小实验:

  1. 在 src/router.ts 中把greeting改名为greet,或把 input 的name字段改成number
  2. 保存后,由于 tsconfig.json 开启了"strict": truetsc(即typecheck脚本)会立刻在 src/client.ts 中报错:Property 'greeting' does not exist,或 input 参数类型不匹配;
  3. 类型错误的根源是 client.ts 第 2 行的import type { AppRouter } from './router'——注意客户端与服务端共享的是同一个router.ts模块而非复制的类型,createTRPCClient<AppRouter>据此精确推导出可访问的 procedure 树和各自的 input/output 类型。

这正是 tRPC “Move Fast and Break Nothing” 的最小证据:破坏性改动(改 procedure 名、改 input 形状)在编译期即被拦截,而不是等到运行时收到 404 或校验失败。

小结

该示例用三个约 20 行的文件(server.ts、router.ts、client.ts)完整覆盖了 tRPC 的核心抽象:initTRPC初始化与嵌套 Router、Zod 可选输入、Express 适配器中间件、createTRPCClient+httpBatchLink的 Node 客户端,以及AppRouter类型驱动的端到端类型检查。适合作为学习 tRPC v11 内部机制的起点;当你需要在浏览器或 React 环境中使用它时,可进一步参考仓库中的其他示例,例如 examples/express-server 与 examples/minimal。

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询