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

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

2026-09-05 14:48:37作者:裴锟轩Denise

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

示例包含什么

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

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384