tRPC — TS 端到端类型安全 RPC
已复核tRPC 是一个让 TypeScript 前后端共享同一份 router 类型的 RPC 框架。日常类比:以前点外卖要先对菜单(独立 schema),再核对后厨是否同一版本;tRPC 是把菜单本身当成类型,编译器替你核对点单。
后端用 initTRPC.create() 建 procedure(可远程调用的小函数)和 router,再 export type AppRouter。前端 createTRPCClient<AppRouter>() 用递归 Proxy 把 user.byId.query(...) 拼成 path,经 HTTP link 发出。运行时没有 codegen 步骤;类型只存在于编译期。
不理解固定 11.18.0 的合同,下面这些事会说错:
- 为什么客户端推荐
createTRPCClient,而旧文里的createTRPCProxyClient只是即将在 v12 删除的别名 - 为什么 input 不只属于 zod——parser 还认 Standard Schema、valibot、arktype、yup、superstruct 和自定义函数
- 为什么 React 现在有两套入口:
@trpc/react-query的 hook,以及@trpc/tanstack-react-query的queryOptions - 为什么 subscription 主合同已经是
AsyncIterable,observable 形态被标 deprecated
固定源码可以把主链拆成五步:
-
根对象只初始化一次:
initTRPC.context<Ctx>().create()产出procedure/middleware/router/mergeRouters/createCallerFactory。非 server 环境默认抛错,除非allowOutsideOfServer: true。 -
procedure builder 逐步收紧类型:
.input(parser).use(mw).output(parser).query|mutation|subscription(resolver)。resolver 拿到ctx、input、signal、path和可选batchIndex。 -
router 是一棵可懒加载的记录:嵌套 router 组成 path;
lazy(() => import(...))按路径延迟加载。服务端调用走createCallerFactory,不经过 HTTP。 -
客户端是 Proxy + link 链:
createTRPCClient把最后一段.query/.mutate/.subscribe映射到 procedure type。固定版本导出httpLink、httpBatchLink、httpBatchStreamLink、httpSubscriptionLink、wsLink、splitLink、loggerLink、retryLink、localLink。httpBatchLink的maxURLLength/maxItems默认Infinity。 -
HTTP 适配是 path 裁剪:
fetchRequestHandler从 URL 去掉endpoint前缀,再交给resolveResponse。同仓还有 Node HTTP、standalone、Express、Fastify、Next、Next App Router、AWS Lambda 与 WebSocket adapter。
案例 1:最小 server + client
Section titled “案例 1:最小 server + client”import { initTRPC } from "@trpc/server";import { createTRPCClient, httpBatchLink } from "@trpc/client";import { z } from "zod";
const t = initTRPC.create();const appRouter = t.router({ user: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query(({ input }) => db.user.find(input.id)), }),});export type AppRouter = typeof appRouter;
const client = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url: "/api/trpc" })],});const user = await client.user.byId.query({ id: "1" });逐部分解释:
export type AppRouter只导出类型,前端import type没有运行时体积- 客户端必须写
.query();mutation 对应.mutate(),subscription 对应.subscribe() - Zod 能用,是因为它暴露
parse/parseAsync;换 Standard Schema 实现同样走getParseFn
案例 2:middleware 收紧 ctx
Section titled “案例 2:middleware 收紧 ctx”const auth = t.middleware(async ({ ctx, next }) => { if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" }); return next({ ctx: { ...ctx, user: ctx.user } });});const protectedProcedure = t.procedure.use(auth);UNAUTHORIZED 在固定源码里是 JSON-RPC -32001。next({ ctx }) 的覆盖会进入下游 resolver 的类型。
案例 3:TanStack Query 的 options 入口
Section titled “案例 3:TanStack Query 的 options 入口”import { createTRPCContext } from "@trpc/tanstack-react-query";import { useQuery } from "@tanstack/react-query";
const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
function UserCard() { const trpc = useTRPC(); const query = useQuery(trpc.user.byId.queryOptions({ id: "1" })); return query.data?.name ?? null;}这是 @trpc/tanstack-react-query@11.18.0 的入口:先拿 queryOptions / mutationOptions,再交给 TanStack Query。@trpc/react-query 的 createTRPCReact + api.user.byId.useQuery 仍在,但已经不是唯一写法。
- 继续把
createTRPCProxyClient写成当前推荐名:它只是 deprecated 别名,源码写明 v12 删除。 - 把 input 说成“只能用 zod”:
getParseFn按~standard/parseAsync/parse/assert/validateSync/create探测,认多家 validator。 - subscription 仍按 observable 教:当前
.subscription()主签名要求AsyncIterable;observable 重载已 deprecated。 - 假设 batch link 默认切批:
maxURLLength与maxItems默认都是无限,不设上限就不会因长度自动拆批。 - 在非 server 环境直接
initTRPC.create():默认会抛;浏览器侧应只用 client 包,或显式允许。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 前后端都是 TypeScript、能共享
AppRouter类型的内部应用 - 需要 compiler 在改字段时立刻打断客户端
- 已有 zod / valibot / arktype 等 runtime parser,并准备接 tanstack-query
不适用:
- 公开多语言 API——类型不能当跨语言契约,应走 OpenAPI / gRPC / connect-rpc
- 需要按字段选择子集的多端 GraphQL 查询——对照 graphql-yoga
- 不能接受 TypeScript
>= 5.7.2,或尚未实测大型 router 的 IDE /tsc成本
固定版本边界
Section titled “固定版本边界”- 本文绑定
trpc/trpc@6aec1578...,@trpc/server/@trpc/client均为11.18.0。 - 未安装依赖、未启动 adapter、未跑上游测试或测量 IDE/bundle,状态保持
UNVERIFIED。 - v12 删除项只按源码 deprecated 标记披露,不预测发布时间。
- 共享语言时,类型可以替代中间 schema 文件——代价是客户端必须能
import type - 推荐入口会比“还能编译的旧名字”更窄——proxy 别名与 observable subscription 都还在,但不该当新项目默认
- parser 适配层比品牌绑定更稳——Standard Schema 让 tRPC 不必把 zod 写进 runtime 依赖
- React 集成正在从“自造 hook 树”退回“给 TanStack 喂 options”
- 新项目该写
createTRPCProxyClient还是createTRPCClient? - 不设
maxItems时,httpBatchLink会按默认 10 条切批吗? - 一个 procedure 的 input 能否用实现了
~standard的非 zod schema?
检查点:
- 应写
createTRPCClient;前者只是 v12 将删除的别名。 - 不会。默认
maxItems与maxURLLength都是Infinity。 - 能。
getParseFn优先识别 Standard Schema。
- 官方文档:trpc.io
- 固定源码:trpc/trpc —— 本文绑定提交
6aec1578a899df50a17e4e78d5512a099b574c18 - zod —— 常见 input parser,但不是唯一适配对象
- tanstack-query ——
@trpc/tanstack-react-query的缓存底座 - graphql-yoga —— 需要字段选择与多语言客户端时的对照
- zod —— procedure input/output 的常见 runtime parser
- tanstack-query ——
queryOptions/ 旧useQueryhook 的缓存层 - graphql-yoga —— schema language + 字段选择的另一条 API 层
- hono —— 同为 TS 优先,但走显式 HTTP 路由
- next-js —— App Router adapter 与 RSC caller 的常见宿主
- connect-rpc —— 浏览器可跑、多语言友好的对照 RPC
- apollo-server —— Apollo Server — Node 端 GraphQL 服务端的事实标准
- arktype —— arktype — schema 长得像 TypeScript 类型本身
- auth-js —— Auth.js — 让 OAuth 登录和会话存储变成两个抽象
- better-auth —— better-auth — 把登录/OAuth/2FA/Passkey 拼成一行配置的 TS 认证框架
- cal-com —— cal.com — 自己能托管的开源 Calendly
- connect-rpc —— ConnectRPC — 让 gRPC 在浏览器里裸跑的 RPC 协议
- effect —— Effect — 给 TypeScript 装上”会跟踪错误和依赖”的副作用引擎
- elysia —— Elysia — 长在 Bun 上的极致类型安全 Web 框架
- fastapi —— FastAPI — 用 Python 类型注解写 API
- gqlgen —— gqlgen — Go 用 schema 先写好再让编译器生成 GraphQL server
- graphql-yoga —— GraphQL Yoga — 跨运行时的轻量 GraphQL 服务器
- grpc-go —— gRPC-Go — Google RPC 框架的官方 Go 实现
- hono —— Hono — 多运行时 Web 框架
- hot-chocolate —— Hot Chocolate — .NET 里 code-first 写 GraphQL 服务器
- next-js —— Next.js — React 全栈框架
- socket-io —— Socket.IO — 让浏览器和 Node.js 像打电话一样互相喊事件
- tanstack-router —— TanStack Router — 把 URL 当类型,编译器替你守路由
- twirp —— Twirp — 用 protobuf 定义服务,但只走 HTTP/1.1 + JSON
- valibot —— Valibot — 拆成乐高的 TypeScript 校验库
- zod —— Zod — TypeScript-first schema 验证