news 2026/9/9 23:24:54

用 tRPC standalone-server 示例从零搭建端到端类型安全的 HTTP + WebSocket 服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 tRPC standalone-server 示例从零搭建端到端类型安全的 HTTP + WebSocket 服务

用 tRPC standalone-server 示例从零搭建端到端类型安全的 HTTP + WebSocket 服务

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

导读

examples/standalone-server是 tRPC 官方维护的"极简(bare-minimum)"参考实现:仅用server.tsclient.ts两个文件,就跑通了Node.js 原生 HTTP 服务器 + WebSocket 订阅双通道,以及基于vanillaTRPCClient(无任何框架依赖)的端到端类型安全调用。通过本文,你将掌握createHTTPServer/applyWSSHandler的最小服务端骨架、客户端splitLink按请求类型分流 HTTP 与 WS 的核心模式,以及 query、mutation、subscription 三种 procedure 的完整调用链与运行/测试方式。


一、示例定位:为什么需要一个"零依赖框架"的独立服务器

tRPC 通常与 Next.js、Express、Fastify 等框架或 Serverless 环境组合使用,但官方文档同时强调:当你想要一个能跑在任何 Node.js 环境、不引入任何 Web 框架的新项目时,Standalone Adapter 是最简单直接的入口——它本质上是"包裹在 Node.js 原生 HTTP Server 之上的一个薄层"。官方还指出,很多生产项目在本地开发时难以直接运行部署形态的适配器(如 Lambda),因此会保留两个入口:本地用 Standalone,部署用其他适配器。

本示例正是这一理念的最小落地,仓库根目录内可直接对照阅读其入口文件 src/server.ts 与 src/client.ts。SKILL 目录中的官方技能文档 adapter-standalone/SKILL.md 也直接将该示例列为sources,进一步印证它承载了 Standalone Adapter 的标准用法。

示例的三个核心特性(源自 README.md):

  • Standalone HTTP 服务器 + WebSocket:同一进程、同一端口同时对外提供 HTTP 请求与 WebSocket 订阅;
  • VanillaTRPCClient:客户端不依赖 React、Next.js 等上层封装,直接用核心包@trpc/client发起调用,适用于纯 Node 脚本、CLI 工具等场景;
  • Bare-minimum:全部代码集中在两个源文件内,无框架、无数据库、无多余目录。

二、运行环境与启动脚本

先看示例的 package.json,了解它的运行时依赖与脚本设计。

关键依赖:

依赖说明
@trpc/server服务端核心包,提供initTRPC与 adapters
@trpc/client客户端核心包,提供createTRPCClient与各 link
wsWebSocket 服务端库(Node 没有内置 WS server,需要单独安装)
zod输入校验(schema 声明)
@trpc/react-query仓库 workspace 统一引入,本示例并未使用

开发依赖方面,示例使用tsx直接运行 TypeScript(免编译)、npm-run-allrun-p并行启动)、wait-port(等待 2022 端口就绪后再启动客户端)、start-server-and-test(测试编排)、esbuild(打包构建)。

npm scripts 一览(均在examples/standalone-server目录内执行):

脚本作用
dev:servertsx watch src/server热重载启动服务端
dev:clientwait-port 2022等待端口,再tsx watch src/client
devrun-p dev:*并行启动 server 与 client
build用 esbuild 将server.ts/client.ts打包为 Node ESM 产物到dist/
typechecktsc做全量类型检查
test-devstart-server-and-test:起服务→探测 2022 端口→跑客户端
test-start等价流程,但运行的是dist/中 esbuild 产物

由于仓库使用 pnpm workspace,从仓库根目录可以直接执行:

pnpm --filter examples-standalone-server dev

或先安装依赖后,在示例目录中运行pnpm dev。沙箱配置 sandbox.config.json 声明了 Node 20 容器,说明该示例面向 Node 20+ 环境,且tsconfig.json使用"type": "module"+ ESM 模块体系运行。

启动顺序上的细节dev:client依赖wait-port 2022,是因为客户端一旦启动就会立刻发起 WebSocket 连接与 query 调用,若服务端未就绪会直接失败;这从侧面说明本示例是"先有服务、后有调用"的同步演示模型。


三、服务端解剖:一个 Router 同时服务 HTTP 与 WS

服务端全部逻辑在 src/server.ts。它展示了 tRPC 服务端的标准三段式:初始化initTRPC→ 声明 Router → 挂载到 Adapter

3.1 Context:HTTP 与 WS 共用的创建函数

// This is how you initialize a context for the server function createContext( opts: CreateHTTPContextOptions | CreateWSSContextFnOptions, ) { return {}; } type Context = Awaited<ReturnType<typeof createContext>>;

这里有个值得学习的细节:HTTP 请求与 WebSocket 连接都会执行 context 创建,两者类型分别是CreateHTTPContextOptions(来自@trpc/server/adapters/standalone)与CreateWSSContextFnOptions(来自@trpc/server/adapters/ws)。为了让一个函数同时兼容两条通道,示例将其参数类型声明为两者的联合类型,再通过Awaited<ReturnType<...>>提取返回类型。实际项目中你可以在这个函数里读取opts.req做鉴权、解析opts.info.connectionParams等,将用户态注入 Context 供各 procedure 使用。

从源码看,standalone.ts 中CreateHTTPContextOptionsNodeHTTPCreateContextFnOptions<http.IncomingMessage, http.ServerResponse>,即提供req/res;而 ws.ts 中CreateWSSContextFnOptionsNodeHTTPCreateContextFnOptions<IncomingMessage, ws.WebSocket>,除req外还透传res(即 WebSocket 实例)与info(含connectionParams与中止信号signal)。

3.2 三种 procedure:query / mutation / subscription

const t = initTRPC.context<Context>().create(); const publicProcedure = t.procedure; const router = t.router; const greetingRouter = router({ hello: publicProcedure .input( z.object({ name: z.string(), }), ) .query(({ input }) => `Hello, ${input.name}!`), }); const postRouter = router({ createPost: publicProcedure .input( z.object({ title: z.string(), text: z.string(), }), ) .mutation(({ input }) => { // imagine db call here return { id: `${Math.random()}`, ...input, }; }), randomNumber: publicProcedure.subscription(() => { return observable<{ randomNumber: number }>((emit) => { const timer = setInterval(() => { // emits a number every second emit.next({ randomNumber: Math.random() }); }, 200); return () => { clearInterval(timer); }; }); }), });

要点归纳:

  • queryhello):入参经 zod 校验为{ name: string },同步返回字符串,走 HTTP GET/POST 语义;
  • mutationcreatePost):接收{ title, text },模拟数据库写入后返回带id的对象——代码注释// imagine db call here明确指出该位置应替换为真实的 DB/ORM 调用;
  • subscriptionrandomNumber):必须返回@trpc/server/observable提供的observable(或异步生成器)。示例每 200msemit.next(...)一个随机数,并在清理函数中clearInterval,这是释放定时器等资源的规范姿势,当客户端取消订阅或断开连接时该清理函数会被执行。

subscription背后的运行时行为可在 ws.ts 中看到:服务端把 observable 转成异步迭代器后逐条转发,客户端通过subscription.stop消息触发服务端abort(见 ws.ts 中对subscription.stop的处理分支)。这正是"订阅取消能及时停止后端推送"的底层保障。

3.3 Router 合并与类型导出

// Merge routers together const appRouter = router({ greeting: greetingRouter, post: postRouter, }); export type AppRouter = typeof appRouter;
  • 嵌套 Router组织命名空间:客户端将以trpc.greeting.hellotrpc.post.randomNumber的路径访问;
  • 导出类型而非实例export type AppRouter = typeof appRouter;是 tRPC 端到端类型安全的枢纽,客户端侧通过import type { AppRouter } from './server'拿到整棵 API 的类型签名。关于 Router 合并与类型导出的更多细节可参考 merging-routers.md 与 procedures.md。

3.4 同一端口上的 HTTP 服务器 + WebSocket 服务器

// http server const server = createHTTPServer({ router: appRouter, createContext, }); // ws server const wss = new WebSocketServer({ server }); applyWSSHandler<AppRouter>({ wss, router: appRouter, createContext, }); server.listen(2022);

这是整个示例最核心的架构技巧:

  1. createHTTPServer({ router, createContext })创建原生 HTTP 服务器并挂载 tRPC 请求处理。从源码看,standalone.ts 的实现极其直白——createHTTPServer就是http.createServer(createHTTPHandler(opts)),即它返回的是一个标准http.Server实例,因此可以调用 Node 原生.listen()
  2. new WebSocketServer({ server })ws的 WebSocketServer 附着在同一个 HTTP server上——HTTP 升级握手与普通请求共享 2022 端口,无需单独开 WS 端口;
  3. applyWSSHandler将 tRPC 的 WS 协议处理绑定到wssconnection事件上。源码中 applyWSSHandler 支持prefix(按路径前缀过滤连接)、keepAlive(心跳保活)、experimental_encoder(自定义线协议编码)等选项,并返回一个带broadcastReconnectNotification()的对象,可用于服务端向所有客户端广播"请重连"的消息。

server.ts 末尾还保留了注释掉的调试片段:wss.clients.size可以实时观察在线客户端数量,实战排查连接问题时非常有用。


四、客户端解剖:splitLink 把"订阅走 WS、其余走 HTTP"自动分流

客户端 src/client.ts 演示的是纯 Node 环境的 vanilla 调用方式。由于 Node 默认不带WebSocket全局对象,第一步要先做 shim:

import { WebSocket } from 'ws'; globalThis.WebSocket = WebSocket as any;

随后创建 WS 客户端并组装 links:

const wsClient = createWSClient({ url: `ws://localhost:2022`, }); const trpc = createTRPCClient<AppRouter>({ links: [ // call subscriptions through websockets and the rest over http splitLink({ condition(op) { return op.type === 'subscription'; }, true: wsLink({ client: wsClient, }), false: httpLink({ url: `http://localhost:2022`, }), }), ], });

这段代码是"HTTP + WS 双通道"在客户端侧的对应物,值得逐行理解:

  • splitLink是一个路由器性质的 link:对每个操作执行condition(op)判断,返回truewsLinkfalsehttpLink
  • 分流规则op.type === 'subscription'表示"只有订阅操作走 WebSocket",query 与 mutation 全部走 HTTP。这与大多数业务场景(订阅是长连接、即时查询是一次性请求)匹配,避免为普通请求长期占用 WS 连接;
  • wsLink/httpLink分别实现 WebSocket 与 HTTP 传输层;createWSClient({ url })负责 WS 连接生命周期管理(断线重连、关闭)。关于 link 体系的更多内容可参考 www/docs/client/links/overview.md。

从 react-query 侧文档 等同仓库资料可以得知,splitLink的这一写法是 tRPC 社区处理"混合传输"的标准模板,也是官网 quickstart 中推荐的模式之一。


五、端到端演示:query、mutation、subscription 依次跑通

main()函数顺序演示了三种 procedure 的完整调用方式:

async function main() { const helloResponse = await trpc.greeting.hello.query({ name: 'world', }); console.log('helloResponse', helloResponse); const createPostRes = await trpc.post.createPost.mutate({ title: 'hello world', text: 'check out https://tRPC.io', }); console.log('createPostResponse', createPostRes); let count = 0; await new Promise<void>((resolve) => { const subscription = trpc.post.randomNumber.subscribe(undefined, { onData(data) { // ^ note that `data` here is inferred console.log('received', data); count++; if (count > 3) { // stop after 3 pulls subscription.unsubscribe(); resolve(); } }, onError(err) { console.error('error', err); }, }); }); await wsClient.close(); } void main();

三个关键观测点:

  1. 类型推断贯穿全链路helloResponse自动推断为stringdata被推断为{ randomNumber: number }——这正是注释datahere is inferred 想强调的效果,也是AppRouter类型从服务端流向客户端的结果。入参的类型约束同样生效:例如给createPost少传字段或给hello传非stringname都会在编译期直接报错。
  2. 订阅的生命周期管理subscribe(undefined, {...})的第一个参数是输入(本例无输入故传undefined);回调中维护计数器,收到 3 条数据后调用subscription.unsubscribe()主动停止并resolve()退出 Promise;随后wsClient.close()关闭 WS 连接,保证 Node 进程能干净退出——如果忘记关闭连接/取消订阅,事件循环会被计时器或连接句柄拖住。
  3. 错误处理路径onError回调负责捕获订阅期间的传输或过程错误,而 query/mutation 的失败则通过await抛出的异常捕获。

执行后预期日志大致为:

helloResponse Hello, world! createPostResponse { id: '0.123456789', title: 'hello world', text: '...' } received { randomNumber: 0.53 } received { randomNumber: 0.87 } ...

六、从"最小示例"到实战:createHTTPHandler、basePath、CORS 与 HTTP/2

最小示例刻意省略了生产环境常见需求,官方文档与技能文档则给出了补齐这些能力的标准配方(见 www/docs/server/adapters/standalone.md 与 packages/server/skills/adapter-standalone/SKILL.md)。以下是可直接迁移的关键模式。

6.1 自定义 HTTP server:createHTTPServer 之外的自由度

createHTTPServer不适合"需要在同一个 HTTP 服务里塞入健康检查、静态资源等自定义逻辑"的场景。此时改用createHTTPHandler返回一个裸的RequestListener,由你自己掌控http.createServer

import { createServer } from 'http'; import { createHTTPHandler } from '@trpc/server/adapters/standalone'; const handler = createHTTPHandler({ router: appRouter, createContext() { return {}; }, }); createServer((req, res) => { if (req.url?.startsWith('/health')) { res.writeHead(200); res.end('OK'); return; } handler(req, res); }).listen(3000);

从源码角度看,createHTTPServer本身就是http.createServer(createHTTPHandler(opts))(standalone.ts),所以这种写法与"最小示例"在 tRPC 处理链路上完全等价,只是把主动权交还给你。这也意味着 src/server.ts 中server.listen(2022)返回的就是标准http.Server.clients.close()等 Node API 均可直接使用。

6.2 basePath:剥离 URL 前缀再路由

当服务需要以/trpc/作为统一前缀对外暴露(例如前置网关按路径分流)时,使用basePath

const handler = createHTTPHandler({ router: appRouter, basePath: '/trpc/', });

basePath会在路由前从请求路径中剥离前缀,使/trpc/greeting.hello仍解析到helloprocedure。注意源码注释强调"务必包含结尾斜杠"(@example '/trpc/'),且默认值为'/'(standalone.ts)。底层实现中 handler 用url.pathname.slice(basePath.length)完成裁剪(见 standalone.ts)。

6.3 CORS:Standalone 默认不做跨域处理

官方文档明确说明:Standalone 服务器默认不响应 HTTP OPTIONS 预检、不设置任何 CORS 头。如果你的客户端跑在浏览器且与 API 不同源(例如本地开发时 Vite dev server 在 5173、API 在 2022),就必须显式处理。最简做法是借助cors包并以middleware选项注入:

npm install cors @types/cors
import cors from 'cors'; createHTTPServer({ middleware: cors({ origin: 'http://localhost:5173' }), router: appRouter, createContext, }).listen(3000);

middleware接受任何 connect/Node 风格中间件函数,但官方提醒它只是一个简单的逃生舱:它不会替你组合多个中间件,若需要多中间件组合,应改用 Express 适配器,或用connect做组合,或直接回到 6.1 的自定义createHTTPHandler方案。关于 Express 对照可参见 www/docs/server/adapters/express.md。

6.4 HTTP/2 与 TLS

需要 HTTP/2 + TLS 时,Standalone 提供createHTTP2Handler与配套的CreateHTTP2ContextOptions(standalone.ts):

import http2 from 'http2'; import { readFileSync } from 'node:fs'; import { createHTTP2Handler } from '@trpc/server/adapters/standalone'; import type { CreateHTTP2ContextOptions } from '@trpc/server/adapters/standalone'; async function createContext(opts: CreateHTTP2ContextOptions) { return {}; } const handler = createHTTP2Handler({ router: appRouter, createContext }); const server = http2.createSecureServer( { key: readFileSync('./certs/server.key'), cert: readFileSync('./certs/server.crt') }, (req, res) => handler(req, res), ); server.listen(3001);

6.5 maxBatchSize:限制单次批量请求条数

tRPC 支持把多个请求打包进一次 HTTP 请求(request batching)。Standalone 提供maxBatchSize上限保护,超过上限的批量请求会收到400 Bad Request

createHTTPServer({ router: appRouter, maxBatchSize: 10, }).listen(3000);

同时应把客户端httpBatchLinkmaxItems设为相同值,避免客户端发出的批次数超过服务端限额。

6.6 WS 增强项

在 ws.ts 中可以看到applyWSSHandler还支持keepAlive(心跳保活,默认关闭,启用后pingMs默认 30s、pongWaitMs默认 5s 未收到 pong 即terminate()断连)、prefix(按 URL 前缀决定是否接受该 WS 连接)、experimental_encoder(自定义线协议编码)等选项。最小示例虽未使用,但它们是真实生产订阅服务(如示例同源的 www/docs/server/subscriptions.md)最常见的调优项。


七、为什么说这是"从零起步"的最佳学习样板

综合来看,examples/standalone-server以两个源文件覆盖了 tRPC 应用的最小闭环:

层次本示例中的落点对应能力
Router 定义greeting/post嵌套路由组织 API 命名空间
Procedure 类型query / mutation / subscriptiontRPC 三类过程全覆盖
服务端传输createHTTPServer+applyWSSHandler共用 2022 端口一次启动双协议
客户端传输splitLink分流wsLink/httpLink按需选择传输层
类型安全闭环export type AppRouterimport type入参/出参全程编译期校验
运行与测试pnpm dev/pnpm test-dev热重载与编排验证

官方文档将 Standalone Adapter 定位为"本地开发与基于服务器的生产环境"的理想起点,同时坦诚其 CORS 缺失、中间件组合能力有限等边界——这恰恰说明它适合学习 tRPC 核心概念与快速原型,而当需要框架级中间件生态时,应转向 adapter-express、adapter-fastify 或面向 Serverless 的 adapter-fetch、adapter-aws-lambda 等适配器。

想要亲手验证端到端流程,可以在示例目录执行pnpm dev(自动先后启动 server 与 client)观察控制台输出;或运行pnpm build && pnpm test-start验证 esbuild 产物同样可用。如果你打算把它作为新项目起点,只需保留server.ts中的 Router 骨架与client.ts中的 link 配置,将hello替换为你的真实业务 procedure 即可。


参考文件索引

  • 示例入口文档:examples/standalone-server/README.md
  • 服务端源码:examples/standalone-server/src/server.ts
  • 客户端源码:examples/standalone-server/src/client.ts
  • 工程配置:examples/standalone-server/package.json
  • Adapter 实现:packages/server/src/adapters/standalone.ts
  • WebSocket handler 实现:packages/server/src/adapters/ws.ts
  • 官方适配器文档:www/docs/server/adapters/standalone.md
  • 技能文档:packages/server/skills/adapter-standalone/SKILL.md

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:24:42

JAVA毕设项目:基于 Java 的小型宠物诊所管理系统的设计与实现 宠物诊所服务管理系统的设计与实现 (源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/9 23:24:30

贾子KLA司法理论(Kucius Theory of KLA Justice)纲要——以逻辑自洽原则为法理根基、逻辑审查先于证据审查为核心的司法公正理论体系

标题 贾子KLA司法理论&#xff08;Kucius Theory of KLA Justice&#xff09;纲要 ——以逻辑自洽原则为法理根基、逻辑审查先于证据审查为核心的司法公正理论体系 摘要 本文提出贾子KLA司法理论纲要——一套以逻辑自洽原则&#xff08;KLA, Logical Consistency Axiom&…

作者头像 李华
网站建设 2026/9/9 23:24:24

激光熔覆三维流速场Comsol仿真建模全流程解析

我前后花了差不多两个月&#xff0c;把激光熔覆的三维流速场模型从零搭到能稳定出结果&#xff0c;中间踩了不少坑。这篇文章把我整个思路、模型设置细节、求解器调参经验都整理出来&#xff0c;如果你正准备用Comsol做激光熔覆相关的多物理场仿真&#xff0c;可以直接照着走。…

作者头像 李华
网站建设 2026/9/9 23:23:12

用CeWL打造定向密码字典:从参数到实战的完整指南

在授权渗透测试里&#xff0c;密码喷洒和弱口令爆破是最常碰到的环节。我发现自己反复面对一个尴尬情况&#xff1a;手头通用字典动辄几个G&#xff0c;但遇到对目标定制化程度要求高的场景&#xff0c;比如只针对某家公司官网的密码喷洒&#xff0c;通用字典反而命中率低得可怜…

作者头像 李华
网站建设 2026/9/9 23:20:34

Redis Zset 详解:有序集合原理、命令与实战场景

搞 Redis 搞到第五篇&#xff0c;终于轮到压轴的 Zset 了。如果你之前已经把 String、List、Hash、Set 都摸过一遍&#xff0c;那 Zset 算是这五兄弟里最"聪明"的一个——它不是简单地存一堆值&#xff0c;而是能让这些值自动排好序&#xff0c;还能快速按名次或分数…

作者头像 李华
网站建设 2026/9/9 23:19:39

健身房自助系统开发,无人值守设备对接技术

健身房自助系统开发&#xff0c;无人值守设备对接技术 24小时无人值守健身房的稳定运营&#xff0c;核心依赖软件系统与线下智能设备的深度联动&#xff0c;区别于传统人工健身房的单一管理模式。无人场景下&#xff0c;门禁闸机、智能电控、灯光能耗、人脸识别终端、场地传感设…

作者头像 李华