首页
/ tRPC 在 Bun 运行时中的端到端实践:从 fetch 适配器到 Vanilla Client

tRPC 在 Bun 运行时中的端到端实践:从 fetch 适配器到 Vanilla Client

2026-09-05 14:47:36作者:姚月梅Lane

本文基于 tRPC 仓库中的 Bun 官方示例(examples/bun),演示如何在 Bun 运行时中搭建一套端到端类型安全的 API:服务端用 @trpc/server/adapters/fetchfetchRequestHandler 挂到 Bun.serve 上,客户端用 createTRPCClient + httpBatchLink 发起带批量能力的请求。读完本文,你将掌握 Bun 场景下 tRPC 的完整落地方式:Router 定义、Context 注入、批量链接的工作原理,以及仓库配套的构建、开发与自动化测试脚本。

示例项目结构与工作区归属

Bun 示例位于仓库的 examples/bun/ 目录,是一个被 pnpm workspace 直接纳入的独立包——pnpm-workspace.yaml 中通过 'examples/*' 规则将其识别为 workspace 成员:

packages:
  - 'packages/*'
  - 'examples/*'
  ...

示例的入口文件与脚本定义如下(package.json):

{
  "name": "examples-bun",
  "private": true,
  "scripts": {
    "build": "bun build src/index.ts --outdir ./dist && bun build src/client.ts --outdir ./dist",
    "dev:server": "bun run src/index.ts --watch",
    "dev:client": "wait-port 3000 && bun run src/client.ts --watch",
    "start:server": "bun run dist/index.js",
    "start:client": "wait-port 3000 && bun run dist/client.js",
    "test-dev": "start-server-and-test 'bun run src/index.ts' 3000 'bun run src/client.ts'",
    "test-start": "start-server-and-test 'bun run dist/index.js' 3000 'bun run dist/client.js'",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@trpc/client": "npm:@trpc/client",
    "@trpc/server": "npm:@trpc/server"
  },
  "devDependencies": {
    "@types/bun": "^1.1.12",
    "eslint": "^9.26.0",
    "start-server-and-test": "^1.12.0",
    "typescript": "^5.9.2",
    "wait-port": "^1.0.1"
  }
}

有两个细节值得注意:

  1. 依赖声明方式@trpc/client@trpc/server 均写作 "npm:@trpc/..." 形式。这是因为该示例位于 pnpm workspace 内,这种写法明确声明“以 npm 包形式解析”(与本地 workspace 包区分),保证示例拿到的是发布产物而非本地源码;
  2. 脚本分层dev:* 系列跑 TS 源码并开启 --watch 热重载,start:* 系列跑 bun build 编译后的 dist/ 产物。dev:clientstart:client 前缀了 wait-port 3000,即先阻塞等待 3000 端口就绪再启动客户端,避免客户端连接时服务端尚未监听。

服务端:Bun.serve 挂载 fetch 适配器

Bun 的 HTTP 服务 API 是 Bun.serve,其签名与 Web fetch API 高度一致(接收一个 fetch(request) 回调),这正好契合 tRPC v11 对 fetch 适配器的设计。示例服务端入口 src/index.ts 全文如下:

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from './router';

Bun.serve({
  port: 3000,
  fetch(request) {
    // Only used for start-server-and-test package that
    // expects a 200 OK to start testing the server
    if (request.method === 'HEAD') {
      return new Response();
    }

    if (new URL(request.url).pathname === '/') {
      return new Response('hello world');
    }

    return fetchRequestHandler({
      endpoint: '/trpc',
      req: request,
      router: appRouter,
      createContext: () => ({}),
    });
  },
});

逐段拆解其职责:

  • 端口与路由分发:服务监听 3000 端口;/ 路径返回 hello world 作为健康探测;其余请求统一交给 fetchRequestHandler 处理;
  • endpoint: '/trpc':声明 tRPC 程序挂载在 URL 前缀 /trpc 之下,客户端后续就会请求 http://127.0.0.1:3000/trpc/hello 这类地址;
  • createContext: () => ({}):这是请求级上下文的注入点。此处示例返回空对象;在实际应用中,你可以在这里从 req(fetch 的 Request 对象)中解析出 session、用户身份等信息。tRPC 的 createContext 会在每个请求执行时调用,其返回值成为所有 procedure 中间件与处理函数的 ctx 参数;
  • HEAD 请求的特殊处理:源码注释说明这是为 start-server-and-test 包准备的——该包在跑测试前会用 HEAD 请求轮询端口,期待一个 200 OK 作为“服务就绪”信号。若不拦截,HEAD 请求会落入 fetchRequestHandler 并因路径不匹配返回 404,导致测试等待超时。

底层原理:fetchRequestHandler 如何解析 URL

fetchRequestHandler 的实现位于 fetchRequestHandler.ts。其中 URL 拆解逻辑揭示了 endpoint 参数如何工作:

const url = new URL(opts.req.url);

const pathname = trimSlashes(url.pathname);
const endpoint = trimSlashes(opts.endpoint);
const path = trimSlashes(pathname.slice(endpoint.length));

即:从请求的完整 pathname 中裁剪掉 endpoint 前缀(/trpc),剩余部分(如 hellopost.createPost)作为 tRPC 的 procedure 路径传给核心的 resolveResponse 做路由分发。此外该文件还处理了 responseMeta 回调——允许 Router 侧通过 ctx.meta 之类的机制自定义响应状态码与响应头,并统一写入 Headers 对象后随 Response 返回。也就是说,无论底层运行时是 Node 的 http 模块还是 Bun 的 Bun.serve,只要它能以 fetch 风格的 Request/Response 与 tRPC 对话,就能复用同一套处理逻辑。

Router 定义:Zod 校验与端到端类型

示例的 src/router.ts 定义了三层结构:

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

let id = 0;

const db = {
  posts: [{ id: ++id, title: 'hello' }],
};

const t = initTRPC.create();

const publicProcedure = t.procedure;
const router = t.router;

const postRouter = router({
  createPost: publicProcedure
    .input(z.object({ title: z.string() }))
    .mutation(({ input }) => {
      const post = { id: ++id, ...input };
      db.posts.push(post);
      return post;
    }),
  listPosts: publicProcedure.query(() => db.posts),
});

export const appRouter = router({
  post: postRouter,
  hello: publicProcedure.input(z.string().nullish()).query(({ input }) => {
    return `hello ${input ?? 'world'}`;
  }),
});

export type AppRouter = typeof appRouter;

要点:

  • initTRPC.create():创建 tRPC 实例,派生出 procedure(procedure 基类)与 router(路由构造器)。未配置 transformer(如 superjson)时,输入输出均为 JSON 序列化;
  • post 子路由:演示了 Router 的嵌套能力,post.createPost(mutation,接收 { title: string } 并返回新建的 post)与 post.listPosts(query,返回整个 posts 数组);
  • hello procedure:输入 z.string().nullish() 表示可省略、可为 null 或字符串,处理函数内用 input ?? 'world' 兜底,正好覆盖“带参/不带参”两种调用形态;
  • AppRouter 类型导出:这是端到端类型安全的枢纽。客户端 createTRPCClient<AppRouter>() 会把整个 Router 树的 procedure 签名(输入类型、返回类型)推导到代理对象的每个方法上,参数写错在编译期即报错,无需任何代码生成。内存中的 db 对象模拟数据库,++id 计数器保证 post 自增主键。

客户端:Vanilla tRPC Client 与批量链接

Vanilla 客户端不依赖任何 UI 框架(React Query 等),适合 Node/Bun 脚本、服务端编排、CLI 等场景。示例的 src/client.ts 完整演示了典型用法:

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

const url = 'http://127.0.0.1:3000/trpc';

const proxy = createTRPCClient<AppRouter>({
  links: [loggerLink(), httpBatchLink({ url })],
});
  • loggerLink() 在前:link 以数组形式串联,loggerLink 作为第一个 link 拦截后续所有操作并打印请求/响应日志,方便观察批量的实际 HTTP 流量;
  • httpBatchLink({ url }):终端 link,负责真正发请求。其实现位于 httpBatchLink.ts,核心机制是:
    • 收集时间窗口内同类型的多个操作,把 path 用逗号拼接成一次 URL(如 /trpc/hello,hello),input 序列化为 JSON 数组,一次 HTTP 请求拿回全部结果(batchOps.map((op) => op.path).join(','));
    • 支持 maxURLLengthmaxItems 两个可选参数(默认均为 Infinity):当拼接后的 URL 超长或操作数过多时,validate 判定批次不成立,自动回退为逐个请求,防止超过 HTTP 服务器的 URL 长度限制;
    • 客户端示例中 await Promise.all([proxy.hello.query(), proxy.hello.query('client')]) 并发发起两个 hello 查询,得益于批处理,二者实际上会被合并为一次 HTTP 请求——这正是 README 中特意安排“parallel queries”这一步的意图;
  • 类型化调用proxy.hello.query() / proxy.hello.query('client') 的入参与返回值类型均由 AppRouter 推导,proxy.post.createPost.mutate({ title }) 的入参必须满足 z.object({ title: z.string() }) 的推断类型;
  • 客户端末尾打印 should be a clean exit if everything is working right,并在 main().catchprocess.exit(1),使该脚本可直接作为端到端冒烟测试的判定依据(退出码 0/1)。

注意 Bun 环境下 import type { AppRouter } from './router.ts' 显式携带 .ts 扩展名,对应 tsconfig.json 中开启的 "allowImportingTsExtensions": true

TypeScript 配置:面向 Bun 的编译选项

示例的 tsconfig.json 是一组典型的“Bun + 现代 ES”配置:

{
  "compilerOptions": {
    "lib": ["esnext"],
    "module": "esnext",
    "target": "esnext",
    "moduleResolution": "bundler",   // TS 5.x+
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "allowJs": true,
    "esModuleInterop": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}

其中 moduleResolution: "bundler" 允许以打包器/Bun 的解析规则处理模块(裸导入、bun:* 内建模块等),配合 @types/bun 提供 Bun.serve 等全局 API 的类型声明;typecheck 脚本仅做 tsc --noEmit 校验,实际编译由 bun build 完成——这是 Bun 生态的常见分工:tsc 负责类型检查,Bun 的打包器负责产物生成。

运行与验证方式

examples/bun/README.md 的指引,前提是先安装 Bun(仓库示例假定本机已具备 Bun 运行时)。在仓库根目录的 workspace 中安装依赖后:

# 终端一:启动服务端(监听 3000 端口,--watch 热重载)
bun dev:server

# 终端二:等待端口就绪后运行客户端
bun dev:client

客户端成功执行会依次打印:两次并发 hello 查询的日志、created post hello clienthas posts [...] first: hello,最后输出干净退出的确认信息。

除手工运行外,示例还提供两条自动化验证路径,均基于 start-server-and-test(这也是服务端要拦截 HEAD 请求的原因):

脚本 流程
test-dev 直接跑 TS 源码:start-server-and-test 'bun run src/index.ts' 3000 'bun run src/client.ts'
test-start bun build 再验证产物:start-server-and-test 'bun run dist/index.js' 3000 'bun run dist/client.js'

即:启动服务端 → 轮询 3000 端口直到就绪 → 运行客户端脚本 → 根据客户端退出码判定成败。这使得该示例既可以作为开发演示,也可以作为 CI 中的一条最小端到端测试链路。

小结

Bun 示例用不到百行代码展示了 tRPC 在“非 Node 框架”环境下的接入范式:只要运行时提供 fetch 风格的请求/响应对象(Bun.servefetch 回调即是),就能通过 fetchRequestHandler 一行接入任意 tRPC Router;客户端侧则以 createTRPCClient + links(loggerLink 观测、httpBatchLink 批处理)完成类型安全的调用。与仓库中 express、fastify、lambda 等其他示例相比,Bun 路径的最大差异仅在于宿主 HTTP 服务由 Bun.serve 承担,其余 Router 定义、类型推导与链接机制完全一致——这也从侧面印证了 tRPC 将适配层与核心逻辑解耦的设计意图。

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

项目优选

收起
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