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-query、next等上层集成,只看 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.0、zod@^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();要点拆解:
createExpressMiddleware({ router })返回一个标准 Express handler,挂载在/trpc前缀下,所有 tRPC 请求都以http://localhost:3000/trpc/<procedure>的形式到达;GET /路由只是为“等待端口就绪”提供的探测端点,与 tRPC 无关;- 适配器本身极薄。从源码 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;几个关键设计:
- 嵌套 router:
hello本身是一个子 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,需要订阅时应改用httpSubscriptionLink或wsLink——这与本示例“纯 query”的定位正好吻合。
端到端类型安全:README 里的“试一试”
README 给出了一条最直观的验证建议(原文):
Tip: Try changing either the procedure name or it's input on the server and see the client type-erroring.
把这条建议展开,可以做一个小实验:
- 在 src/router.ts 中把
greeting改名为greet,或把 input 的name字段改成number; - 保存后,由于 tsconfig.json 开启了
"strict": true,tsc(即typecheck脚本)会立刻在 src/client.ts 中报错:Property 'greeting' does not exist,或 input 参数类型不匹配; - 类型错误的根源是 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),仅供参考