首页
/ tRPC 与 Fastify 集成实战:HTTP 路由、插件选项与 WebSocket 订阅完整指南

tRPC 与 Fastify 集成实战:HTTP 路由、插件选项与 WebSocket 订阅完整指南

2026-09-08 16:10:43作者:鲍丁臣Ursa

说明:本文基于本仓库中 tRPC v9 官方文档 fastify.md 编写,并对照仓库内当前 Fastify 适配器源码与示例工程进行纵深印证。文中既保留 v9 时代经典 API 写法(trpc.router() + 字符串路径),也展示当前仓库示例中的新写法(initTRPC + 链式调用),便于读者在阅读旧文档与维护现代代码时都能对号入座。

tRPC 官方为 Fastify 提供了开箱即用的适配器,可把你的类型安全路由器转换成标准的 Fastify 插件,从而在同一进程中同时暴露 HTTP 端点与基于 @fastify/websocket 的实时订阅通道。读完本文,你将掌握:从零搭建 @trpc/server + Fastify 服务端、编写带 Zod 校验的路由器与按请求创建的 Context、理解 fastifyTRPCPlugin 各项插件选项(prefix / useWSS / trpcOptions)的底层行为,并通过 splitLink 让客户端把普通请求走 HTTP、把订阅走 WebSocket。

示例工程:快速入手的首选方式

学习 Fastify 适配器最有效的方式是直接运行仓库中的示例应用。本文所依据的 v9 文档推荐的示例工程在 examples/fastify-server 目录,工程结构清晰分层:

该示例同时覆盖"Fastify 服务 + WebSocket 订阅"与"Node 中的简单 tRPC 客户端"两块内容,是理解本文全部要点的最小可运行闭环。

如何在 Fastify 中集成 tRPC

安装依赖

yarn add @trpc/server fastify zod

Zod 并不是 tRPC 的强制依赖,但下面的示例路由器会用到它来做输入校验。Fastify 的版本需要与 Node 运行环境匹配;如果后续要启用 WebSocket 订阅,还需满足 @fastify/websocket 对 Fastify 的最低版本要求(见下文订阅章节)。

创建路由器(Router)

首先需要一个用于处理 query、mutation 和 subscription 的路由器。下面的示例代码保存为 router.ts(沿用 v9 文档的经典 API 写法,字符串路径 + .query / .mutation):

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

type User = {
  id: string;
  name: string;
  bio?: string;
};

const users: Record<string, User> = {};

export const appRouter = trpc
  .router()
  .query('getUserById', {
    input: z.string(),
    async resolve({ input }) {
      return users[input]; // input type is string
    },
  })
  .mutation('createUser', {
    // validate input with Zod
    input: z.object({
      name: z.string().min(3),
      bio: z.string().max(142).optional(),
    }),
    async resolve({ input }) {
      const id = Date.now().toString();
      const user: User = { id, ...input };
      users[user.id] = user;
      return user;
    },
  });

// export type definition of API
export type AppRouter = typeof appRouter;

示例中的两个 procedure 展示了 tRPC 的核心收益:

  • getUserByIdz.string() 声明输入,resolveinput 会被自动推断为 string 类型;
  • createUserz.object({...}) 做输入校验,非法请求会在进入业务逻辑前被拦截;
  • 最后 export type AppRouter = typeof appRouter; 把路由器类型导出,客户端可据此获得端到端的类型安全。

router.ts 变得臃肿时,官方建议把路由拆成多个子路由器、各自独立成文件,再通过 合并路由 组装为单一根 appRouter。路由器的定义与调用机制可参考 v9 路由器文档

新旧 API 对照:仓库内示例 examples/fastify-server/src/server/router/trpc.ts 展示的是当前主流的 initTRPC 写法——先 initTRPC.context<Context>().create(...) 得到 t,再用 t.router / t.procedure 组合路由,子路由聚合见 examples/fastify-server/src/server/router/index.ts。两种写法对适配器完全透明,fastifyTRPCPlugin 只关心最终导出的 appRouter

创建 Context

接着需要定义一个在每个请求时都会执行一次的 Context。示例保存为 context.ts

import { inferAsyncReturnType } from '@trpc/server';
import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';

export function createContext({ req, res }: CreateFastifyContextOptions) {
  const user = { name: req.headers.username ?? 'anonymous' };

  return { req, res, user };
}

export type Context = inferAsyncReturnType<typeof createContext>;

要点解析:

  • CreateFastifyContextOptions@trpc/server/adapters/fastify 导出,其内部类型为 NodeHTTPCreateContextFnOptions<FastifyRequest, FastifyReply>(参见 fastifyTRPCPlugin.ts),因此 req 是完整的 Fastify 请求对象,res 是 Fastify 回复对象;
  • 这里演示了从请求头读取 username 并兜底为 anonymous——生产场景中通常在这里做鉴权、解析会话并把用户对象注入 Context;
  • 关于 Context 的完整语义(何时创建、如何在 procedure 中使用)见 v9 Context 文档。仓库示例 examples/fastify-server/src/server/router/context.ts 则使用 Awaited<ReturnType<typeof createContext>> 推导类型,同样可行。

创建 Fastify 服务器并注册插件

tRPC 内置了 Fastify 适配器,它能把 tRPC 路由器转换成 Fastify 插件。为了在大批量(batch)请求时避免出错,需要把 Fastify 的 maxParamLength 选项设置为合适的较大值(见下方示例中的 5000):

import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import fastify from 'fastify';
import { createContext } from './context';
import { appRouter } from './router';

const server = fastify({
  maxParamLength: 5000,
});

server.register(fastifyTRPCPlugin, {
  prefix: '/trpc',
  trpcOptions: { router: appRouter, createContext },
});

(async () => {
  try {
    await server.listen({ port: 3000 });
  } catch (err) {
    server.log.error(err);
    process.exit(1);
  }
})();

设置 maxParamLength 的原因:tRPC 支持一次 HTTP 请求中打包多个 procedure 调用(HTTP batching),批量请求的 URL 中会携带编码后的输入参数,URL 会明显变长,超过 Fastify 默认上限会导致请求被拒。放宽上限可避免大批量请求出错。仓库示例 examples/fastify-server/src/server/server.ts 采用了同样的装配方式(fastify({ logger: dev }) 并注册 wsfastifyTRPCPlugin),可作为对照。

注册完成后,你的端点即可通过 HTTP 访问:

端点 HTTP URI
getUserById GET http://localhost:3000/trpc/getUserById?input=INPUT

其中 INPUT 是 URI 编码后的 JSON 字符串。
createUser POST http://localhost:3000/trpc/createUser

请求体 req.body 类型为 User

插件内部到底做了什么? 阅读适配器源码可以更准确地理解上面这段配置:fastifyTRPCPlugin.ts 的核心行为可归纳为:

  1. 移除 Fastify 默认的 application/jsonmultipart/form-data 内容类型解析器,改为 parseAs: 'string' 的原样字符串解析——让 tRPC 自行处理请求体(batching、输入解析等都由 tRPC 内部完成),而不是让 Fastify 提前把 JSON 反序列化掉;
  2. 通过 fastify.all(\${prefix}/:path`)注册一个通配路由,把所有方法与子路径统一交给 [fastifyRequestHandler.ts](https://gitcode.com/GitHub_Trending/tr/trpc/blob/340811ba5320637fbaf48fccf3dbfdd258bd34db/packages/server/src/adapters/fastify/fastifyRequestHandler.ts?utm_source=gitcode_repo_files#L43-L80),后者把req.raw(Node IncomingMessage)转成统一的请求对象、调用 createContext、执行 resolveResponse,最后 res.send(res)` 返回结果;
  3. prefix 未提供,tRPC 会使用空字符串前缀(路由交由 Fastify 插件机制处理)。

如何启用订阅(WebSocket)

Fastify 适配器通过 @fastify/websocket 插件支持订阅。相比上面的步骤,只需额外做四件事:安装依赖、在路由器中加入订阅、在插件上激活 useWSS 选项、注册 @fastify/websocket。注意 @fastify/websocket 要求 Fastify 最低版本为 3.11.0

安装依赖

yarn add @fastify/websocket

导入并注册 @fastify/websocket

import ws from '@fastify/websocket';

server.register(ws);

添加一些订阅

回到上一步创建的 router.ts,追加一个订阅:

export const appRouter = trpc
  .router()
  // .query(...)
  // .mutation(...)
  .subscription('randomNumber', {
    resolve() {
      return new Subscription<{ randomNumber: number }>((emit) => {
        const timer = setInterval(() => {
          emit.data({ randomNumber: Math.random() });
        }, 1000);
        return () => {
          clearInterval(timer);
        };
      });
    },
  });

上面的 Subscription 是 v9 时代的发布器抽象:回调函数里通过 emit.data(...) 周期性地推送数据,返回的清理函数负责在订阅结束(如客户端断开)时 clearInterval,避免定时器泄漏。在当前仓库示例中,订阅改用了 @trpc/server/observableobservable 工厂,语义完全一致——推送用 emit.next、清理返回 clearInterval(timer),见 sub.ts。关于订阅的更完整概念(重连、鉴权、取消等)可参考 订阅文档(面向当前版本)或 v9 WebSocket 相关章节

激活 useWSS 选项

server.register(fastifyTRPCPlugin, {
  useWSS: true,
  // ...
});

现在你可以订阅 randomNumber 这个主题,每秒都会收到一个随机数 🚀。

从源码看,当 useWSS: true 时,插件会调用 getWSConnectionHandler 生成 WebSocket 连接处理器,并在 prefix 对应的路径上以 { websocket: true } 方式注册 GET 路由,见 fastifyTRPCPlugin.ts;若配置了心跳保活(当前源码中通过 trpcOptions.keepAlivepingMs / pongWaitMs 控制),还会额外启动 keep-alive 逻辑。这意味着订阅与普通 HTTP 调用可以共用同一端口、同一 prefix,由客户端依据调用类型自动分流。

Fastify 插件选项一览

fastifyTRPCPlugin 接受的插件选项如下(与 v9 文档一致,类型定义见 fastifyTRPCPlugin.ts):

名称 类型 必填 默认值 说明
prefix string 可选 "/trpc" HTTP 与 WebSocket 的统一路径前缀
useWSS boolean 可选 false 是否启用 WebSocket 订阅支持
trpcOptions NodeHTTPHandlerOptions 必填 不适用 需至少包含 routercreateContext,并可传递 onErrorbatchingtransformer 等底层 HTTP 处理选项

其中 trpcOptions 是必选项,类型对应 NodeHTTPHandlerOptions(当前源码中细化为 FastifyHandlerOptions = HTTPBaseHandlerOptions & NodeHTTPCreateContextOption,见 fastifyRequestHandler.ts),它决定 tRPC 如何解析请求、创建上下文与返回响应。

客户端联通:HTTP 与 WebSocket 自动分流

服务端装配完成后,客户端如何在一次请求中决定走 HTTP 还是 WebSocket?仓库示例 examples/fastify-server/src/client/index.ts 给出了标准做法——用 splitLink 按操作类型分流:

import {
  createTRPCClient,
  createWSClient,
  httpBatchLink,
  splitLink,
  wsLink,
} from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from '../server/router';

const urlEnd = `localhost:3000/trpc`;
const wsClient = createWSClient({ url: `ws://${urlEnd}` });

const trpc = createTRPCClient<AppRouter>({
  links: [
    splitLink({
      condition(op) {
        return op.type === 'subscription';
      },
      true: wsLink({ client: wsClient, transformer: superjson }),
      false: httpBatchLink({
        url: `http://${urlEnd}`,
        transformer: superjson,
      }),
    }),
  ],
});

const version = await trpc.api.version.query();          // 走 HTTP
const hello = await trpc.api.hello.query();              // 走 HTTP
await trpc.posts.reset.mutate();                         // 走 HTTP

await new Promise<void>((resolve) => {
  const sub = trpc.sub.randomNumber.subscribe(undefined, {
    onData(data) {
      // 走 WebSocket,约每秒收到一个 { randomNumber }
    },
  });
});

这段代码的关键点:

  • 服务端与客户端的 URL 采用同一 端口 + prefix(即 http(s)://host:port/trpcws(s)://host:port/trpc)——这正是 server.ts 中注册插件时 prefix: '/trpc' 的含义,确保客户端可预测端点地址;
  • splitLinkcondition 判定 op.type === 'subscription' 时走 wsLink,其余(query / mutation)走 httpBatchLink,从而在一个客户端中统一管理两类请求;
  • transformer: superjson 与示例服务端 trpc.ts 中声明的 transformer: superjson 遥相呼应,实现跨端的数据序列化一致(服务端、客户端两侧 transformer 必须匹配)。

小结

tRPC 的 Fastify 适配器把"类型安全的端到端 RPC"接入成熟的 Fastify 生态只需三步:定义 appRouter、编写 createContext、用 fastifyTRPCPlugin 注册插件。需要实时推送时,追加 @fastify/websocket 并把插件选项 useWSS 设为 true。无论是 v9 文档 中的经典 Subscription API,还是 示例工程 中当前的 initTRPC + observable 写法,适配器层的行为与插件选项均保持稳定一致,可放心按本文方案落地到实际项目。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
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
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525