首页
/ 在 Express.js 中接入 tRPC:使用官方适配器构建类型安全的端到端 API(v9.x 实战指南)

在 Express.js 中接入 tRPC:使用官方适配器构建类型安全的端到端 API(v9.x 实战指南)

2026-09-08 18:44:02作者:裘旻烁

本指南基于 tRPC 官方 v9.x 文档《Usage with Express.js》编写,讲解如何把 tRPC 无缝接入既有 Express.js 项目:从安装依赖、定义类型安全的路由,到通过内置 Express 适配器将整个 Router 挂载为中间件,最终获得可被 HTTP 直接调用的 API 端点。读完本文,你将掌握 createExpressMiddleware 的完整接入流程、Context 的注入方式,并能参照仓库内的完整示例工程直接上手。

背景:tRPC 如何与 Express.js 结合

tRPC 的核心价值在于端到端类型安全:在服务端用 TypeScript 定义 Router 后,客户端可以在不编写任何接口文档、不做手工类型声明的情况下,获得与后端完全一致的自动补全与编译期类型检查。Express.js 是 Node.js 生态中最常用的 HTTP 框架之一,因此 tRPC 官方内置了针对 Express 的适配器,把 Router 包装成标准的 Express 中间件,使其可以与其他 Express 路由、中间件生态共存。

仓库中与本主题强相关的配套资源包括:

版本说明:本文正文严格遵循 v9.x 文档所对应的 @trpc/server v9 API(trpc.router() 链式调用风格);文末会给出当前仓库(v10/v11 时代,使用 initTRPC)的写法差异提示。本文中引用的源码均以当前仓库实际内容为准。

1. 安装依赖

在已有 Express.js 项目的根目录中执行:

yarn add @trpc/server zod

同样可以使用 npm install @trpc/server zodpnpm add @trpc/server zod 完成安装。

其中 Zod 并不是必需的依赖,它只是下面示例 Router 中用来做输入校验的库。tRPC 的校验层是可替换的,若你的项目已经使用其他校验方案(或不需要输入校验),可以不安装 Zod。

2. 创建 tRPC Router

首先定义 API 的类型与逻辑。一个典型的 Router 同时包含查询(query)与变更(mutation)两类 Procedure,分别对应 HTTP 语义中的"读"与"写":

import * as trpc from '@trpc/server';
import { z } from 'zod';

const appRouter = trpc
  .router()
  .query('getUser', {
    input: z.string(),
    async resolve(req) {
      req.input; // string
      return { id: req.input, name: 'Bilbo' };
    },
  })
  .mutation('createUser', {
    // 用 Zod 校验输入
    input: z.object({ name: z.string().min(5) }),
    async resolve(req) {
      // 这里可以接入任意 ORM
      return await UserModel.create({
        data: req.input,
      });
    },
  });

// 导出 API 的类型定义,客户端将基于它获得类型推断
export type AppRouter = typeof appRouter;

这段代码的关键点:

  • .query('getUser', ...) 定义只读操作,输入为 z.string(),其 resolve 回调内 req.input 已被推断为 string 类型;
  • .mutation('createUser', ...) 定义写入操作,输入经 z.object({ name: z.string().min(5) }) 校验(长度不足 5 的请求会在进入 resolve 前被自动拒绝);
  • export type AppRouter = typeof appRouter 是 tRPC 类型安全链路的源头——客户端 import type { AppRouter } from './server' 后即可获得完整类型。

关于路由文件膨胀:当 Router 文件过大时,应当把路由拆分为多个子 Router,每个子 Router 放在独立文件中,最后通过合并(merge)组合成一个根 appRouter。合并方式详见 v9 文档 merging-routers.md 以及 v9 Context 说明 context.md。仓库示例 examples/express-server/src/server.ts 演示了更完整的拆分子路由(postRoutermessageRouter、内联的 admin 路由)并合并进根 Router 的写法。

3. 使用 Express.js 适配器挂载路由

tRPC 开箱即用地包含了 Express 适配器(@trpc/server/adapters/express),它能把 Router 转换成一个标准的 Express 中间件:

import * as trpcExpress from '@trpc/server/adapters/express';

const appRouter = /* ... */;

const app = express();

// 该函数会在每次请求时被调用,用于构造 Context
const createContext = ({
  req,
  res,
}: trpcExpress.CreateExpressContextOptions) => ({}) // no context
type Context = trpc.inferAsyncReturnType<typeof createContext>;

app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({
    router: appRouter,
    createContext,
  })
);

app.listen(4000);

接入过程只需要三步,理解下面三个核心概念即可:

  1. createExpressMiddleware:将 Router 与 Context 工厂包装成 Express 中间件,挂载在路径前缀 /trpc 下。参数对象中的 router 即上一步定义的 appRouter
  2. createContextCreateExpressContextOptions:该工厂函数在每个请求到来时执行一次,接收 Express 的 reqres 对象。你可以在其中解析 req.headers.authorization、读取 session、创建数据库连接等,返回值会成为所有 Procedure 的 ctx。上例返回空对象,即"无 Context"。
  3. trpc.inferAsyncReturnType<typeof createContext>:把 createContext 的异步返回类型推导为 Context 类型,供后续给 Router 标注 Context 使用(v9 中配合 trpc.router<Context>() 显式声明)。

examples/express-server/src/server.ts 中可以看一个带鉴权的 createContext 实战写法:它读取 req.headers.authorization,当值不为 'secret' 时返回 user: null,否则返回用户对象;随后 admin.secret 这类受保护 Procedure 依赖该 ctx.user 抛出 TRPCErrorUNAUTHORIZED / FORBIDDEN)。

端点如何映射到 HTTP

挂载完成后,你的 Procedure 立刻可以通过 HTTP 访问:

端点 HTTP URI
getUser GET http://localhost:4000/trpc/getUser?input=INPUT,其中 INPUT 是 URI 编码后的 JSON 字符串
createUser POST http://localhost:4000/trpc/createUserreq.body 形如 {name: string}(且需满足 min(5) 校验)

Procedure 的完整路径 = 挂载前缀 /trpc + Procedure 名称GET 请求通过 ?input= 传递 URI 编码的 JSON 输入;POST 请求则把输入放在请求体中。tRPC 的 HTTP 层会自动完成序列化/反序列化与 Zod 校验,非法输入会返回结构化的错误响应。

4. 适配器源码原理:中间件背后发生了什么

以当前仓库的适配器实现为参照,packages/server/src/adapters/express.ts 展示了这条链路的本质:

export function createExpressMiddleware<TRouter extends AnyRouter>(
  opts: NodeHTTPHandlerOptions<TRouter, express.Request, express.Response>,
): express.Handler {
  return (req, res) => {
    let path = '';
    run(async () => {
      // 从完整请求路径中截取 Procedure 名
      path = req.path.slice(req.path.lastIndexOf('/') + 1);
      await nodeHTTPRequestHandler({ ...opts, req, res, path });
    }).catch(internal_exceptionHandler({ req, res, path, ...opts }));
  };
}

从中可以看出三点实现事实:

  • createExpressMiddleware 的返回值就是一个 express.Handler(req, res) => void),因此可以直接传给 app.use,也可以作为子中间件与其他 Express 路由自由组合;
  • 中间件内部从 req.path 的最后一个 / 之后截取路径段作为 Procedure 名,这就是为什么挂载前缀与 Procedure 名必须拼成 /<prefix>/<procedureName> 的形式;
  • 真正的请求处理委托给 node-http 模块中的 nodeHTTPRequestHandler,这意味着 Express 适配器与独立服务器(standalone)、其它 Node HTTP 适配器共享同一套 HTTP 处理核心;出错时通过 internal_exceptionHandler 统一处理异常并写出错误响应。

5. 完整可运行示例与客户端调用

精简版(express-minimal)

examples/express-minimal 是最小可运行示例:

  • 路由定义 src/router.ts 使用 initTRPC 风格(t.router({...}))定义了一个带可选 name 入参的 hello.greeting 查询;
  • 服务端 src/server.ts 额外保留了一个常规 Express 路由 GET /(用于健康检查/测试等待),再把 tRPC 中间件挂载到 /trpc,最后 app.listen(3000)

这展示了一种常见形态:tRPC 中间件与既有 Express 路由并存,两者互不干扰。

完整版(express-server)

examples/express-server 则覆盖了更多真实场景,包括订阅式消息推送(基于 Node EventEmitter)、分层子路由、受鉴权保护的 admin.secret 等。其客户端 src/client.ts 演示了 tRPC 客户端的典型用法:

import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client';
import type { AppRouter } from './server';

const trpc = createTRPCClient<AppRouter>({
  links: [
    loggerLink(),
    httpBatchLink({ url: `http://localhost:2021/trpc` }),
  ],
});

注意 AppRouter 是从服务端 import type 而来的——这正是"类型安全的端到端"的落点:客户端不用手写任何接口签名。此外该示例还展示了:

  • 批处理httpBatchLink 可以把多个查询合并为单个 HTTP 请求(示例中 Promise.all 并发调用两次 hello.query);
  • 按请求注入 HeaderhttpBatchLinkheaders 选项返回 { authorization: 'secret' },与服务端 createContext 的鉴权逻辑一一对应,从而解锁 admin.secret 查询;未携带该 Header 时则被服务端以 TRPCError 拒绝。

6. 与既有 Express 路由共存时的最佳实践

基于 packages/server/skills/adapter-express/SKILL.md 与仓库示例,接入时建议注意以下几点:

6.1 常规 REST 路由与 tRPC 共存

无需把整个应用都迁移到 tRPC。健康检查、静态资源、回调接口等仍可用原生 Express 路由,再把 /trpc 前缀交给 tRPC 适配器即可。示例如下(同时可叠加 cors() 等 Express 生态中间件):

import * as trpcExpress from '@trpc/server/adapters/express';
import cors from 'cors';
import express from 'express';
import { createContext } from './context';
import { appRouter } from './router';

const app = express();

app.use(cors());

app.get('/health', (_req, res) => {
  res.json({ status: 'ok' });
});

app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({
    router: appRouter,
    createContext,
  }),
);

app.listen(4000);

6.2 不要在 tRPC 之前全局注册 express.json()

高优先级提醒:如果先全局执行 app.use(express.json()) 再挂载 tRPC 中间件,全局 body 解析器会先消费并解析请求体,导致 tRPC 收到的请求体已被改写,从而破坏 multipart/form-data(FormData)与二进制内容类型的处理。推荐做法是只把 JSON body 解析器限定在非 tRPC 路由上:

const app = express();
// 仅对 /api 下的非 tRPC 路由启用 body 解析
app.use('/api', express.json());
app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({ router: appRouter, createContext }),
);

这也解释了为何示例工程 examples/express-server/src/server.ts 中没有对 /trpc 启用全局 express.json()

6.3 控制批处理上限 maxBatchSize

若你的客户端启用了批处理,可在服务端中间件选项中设置 maxBatchSize,防止单个请求携带过多操作:

app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({
    router: appRouter,
    createContext,
    maxBatchSize: 10,
  }),
);

超过 maxBatchSize 的批处理请求会被以 400 Bad Request 拒绝。客户端侧应同步把 httpBatchLinkmaxItems 设为相同数值,避免请求超限。

7. 版本演进提示:v9 与当前 v10/v11 写法的差异

v9.x 文档中的 Router 使用 trpc.router().query(...) 链式风格。在 v10/v11(即本仓库当前主线的 packages/server 与现行文档 www/docs/server/adapters/express.md)中,写法统一改为先 initTRPC 初始化实例,再通过 t.router / t.procedure 定义,例如:

import { initTRPC } from '@trpc/server';
import * as trpcExpress from '@trpc/server/adapters/express';

const t = initTRPC.context<Context>().create();
const appRouter = t.router({
  greet: t.procedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});

适配器的接入形态没有本质变化createExpressMiddlewarecreateContextCreateExpressContextOptionsapp.use('/trpc', ...) 的核心用法与本文一致(express-minimalexpress-server 两个示例即运行在当前写法之上)。从 v10 迁移到 v11 的完整细节可参考 migrate-from-v10-to-v11.mdx

总结

在 Express.js 项目中接入 tRPC 只需要三步:安装依赖 → 定义(可拆分的)Router → 用 createExpressMiddleware 将其挂载到 /trpc 前缀。整个过程不需要额外启动独立服务,所有 Procedure 立即成为可被 HTTP 访问的端点,同时客户端借由 AppRouter 类型获得零成本的端到端类型安全。若想直接运行体验,参考 examples/express-minimalexamples/express-server 两个示例工程的 package.json 脚本即可快速启动。

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

项目优选

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