tRPC 与 Fastify 集成实战:HTTP 路由、插件选项与 WebSocket 订阅完整指南
说明:本文基于本仓库中 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 目录,工程结构清晰分层:
- 服务端入口 examples/fastify-server/src/server/index.ts:读取配置并启动服务;
- 服务装配 examples/fastify-server/src/server/server.ts:注册 WebSocket 插件与 tRPC 插件;
- 路由与 Context 分别位于 examples/fastify-server/src/server/router 下;
- 简单的 Node 端 tRPC 客户端位于 examples/fastify-server/src/client/index.ts;
- 工程说明见 examples/fastify-server/README.md,其
package.json中提供了启动脚本。
该示例同时覆盖"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 的核心收益:
getUserById用z.string()声明输入,resolve中input会被自动推断为string类型;createUser用z.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 }) 并注册 ws 与 fastifyTRPCPlugin),可作为对照。
注册完成后,你的端点即可通过 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 的核心行为可归纳为:
- 移除 Fastify 默认的
application/json与multipart/form-data内容类型解析器,改为parseAs: 'string'的原样字符串解析——让 tRPC 自行处理请求体(batching、输入解析等都由 tRPC 内部完成),而不是让 Fastify 提前把 JSON 反序列化掉; - 通过
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(NodeIncomingMessage)转成统一的请求对象、调用createContext、执行resolveResponse,最后res.send(res)` 返回结果; - 若
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/observable 的 observable 工厂,语义完全一致——推送用 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.keepAlive 的 pingMs / pongWaitMs 控制),还会额外启动 keep-alive 逻辑。这意味着订阅与普通 HTTP 调用可以共用同一端口、同一 prefix,由客户端依据调用类型自动分流。
Fastify 插件选项一览
fastifyTRPCPlugin 接受的插件选项如下(与 v9 文档一致,类型定义见 fastifyTRPCPlugin.ts):
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
prefix |
string |
可选 | "/trpc" |
HTTP 与 WebSocket 的统一路径前缀 |
useWSS |
boolean |
可选 | false |
是否启用 WebSocket 订阅支持 |
trpcOptions |
NodeHTTPHandlerOptions |
必填 | 不适用 | 需至少包含 router 与 createContext,并可传递 onError、batching、transformer 等底层 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/trpc与ws(s)://host:port/trpc)——这正是 server.ts 中注册插件时prefix: '/trpc'的含义,确保客户端可预测端点地址; splitLink的condition判定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 写法,适配器层的行为与插件选项均保持稳定一致,可放心按本文方案落地到实际项目。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00