跳至正文

tRPC Router 与 Procedure

本文覆盖 tRPC 服务端和客户端的核心组件:Router、Procedure、Context、Middleware、Client。示例基于 @trpc/server / @trpc/client v11,前后端同 monorepo。

初始化

所有 tRPC 项目从初始化一个 t 实例开始。t 是 Router / Procedure / Middleware 的工厂。

import { initTRPC } from "@trpc/server";

export const t = initTRPC.create();

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

带上下文类型的版本(推荐):

import { initTRPC } from "@trpc/server";
import type { Context } from "./context";

export const t = initTRPC.context<Context>().create();

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

Router

Router 是组织 Procedure 的容器,支持无限嵌套。

import { router, publicProcedure } from "./trpc";
import { z } from "zod";

export const userRouter = router({
  list: publicProcedure.query(async ({ ctx }) => ctx.db.user.findMany()),
  byId: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) =>
      ctx.db.user.findUnique({ where: { id: input.id } }),
    ),
});

export const postRouter = router({
  list: publicProcedure.query(async ({ ctx }) => ctx.db.post.findMany()),
});

// 顶层 Router:组合子 Router
export const appRouter = router({
  user: userRouter,
  post: postRouter,
});

export type AppRouter = typeof appRouter;

客户端调用:

trpc.user.list.query();
trpc.user.byId.query({ id: "1" });
trpc.post.list.query();

Note

要点

  • AppRouter 只导出类型export type),不导出运行时代码,避免客户端打包服务端依赖
  • 子 Router 按业务域拆分(userpostorder)保持可维护
  • 路径深度不限,但建议不超过 3 层

Procedure

Procedure 是单个 API 端点。分两种:

类型用途语义
query读数据幂等,客户端可缓存
mutation写数据非幂等,TanStack Query 默认不缓存
const getUser = publicProcedure
  .input(z.object({ id: z.string() }))
  .query(async ({ input, ctx }) => {
    return ctx.db.user.findUnique({ where: { id: input.id } });
  });

const createUser = publicProcedure
  .input(z.object({ name: z.string(), email: z.string().email() }))
  .mutation(async ({ input, ctx }) => {
    return ctx.db.user.create({ data: input });
  });

input / output

  • .input(zodSchema):Zod 校验客户端传入参数,失败自动抛 BAD_REQUEST
  • .output(zodSchema)(可选):在开发期校验返回值符合约定,生产环境可关掉省开销
const safeProfile = publicProcedure
  .input(z.object({ id: z.string() }))
  .output(z.object({ id: z.string(), name: z.string() }))
  .query(async ({ input, ctx }) => {
    const u = await ctx.db.user.findUnique({ where: { id: input.id } });
    return { id: u!.id, name: u!.name };
  });

Note

要点

Zod 校验是 tRPC 的”类型与运行时”的桥:TypeScript 只保证编译期,Zod 保证实际收到的数据真的是 { id: string }。不加 .input() 的 procedure 客户端传什么都会原样转给 handler,运行时没有防护。

Context

Context 是每次请求共享的对象,通常放数据库连接、session、DataLoader 等。

// context.ts
import { db } from "./db";
import { getSession } from "./auth";

export async function createContext({ req }: { req: Request }) {
  const session = await getSession(req);
  return {
    db,
    session,
    user: session?.user ?? null,
  };
}

export type Context = Awaited<ReturnType<typeof createContext>>;

挂载到 HTTP handler(Next.js App Router 示例):

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/router";
import { createContext } from "@/server/context";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => createContext({ req }),
  });

export { handler as GET, handler as POST };

Note

要点:Fetch adapter 只有 req

Next.js App Router / Cloudflare Workers / Deno / Bun 等 Fetch 风格运行时没有 res 对象,响应通过 Response 返回值表达。Pages Router 或 Express adapter 才会传入 { req, res }。写 createContext 签名时按目标 adapter 选参数,不要凑齐两个。

Middleware

Middleware 在 procedure 执行前后插入逻辑,常用于鉴权、日志、性能监控。

鉴权示例

const isAuthed = middleware(({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({
    ctx: {
      // 重写 ctx 类型:user 不再是 nullable
      user: ctx.user,
    },
  });
});

export const protectedProcedure = publicProcedure.use(isAuthed);
const updateProfile = protectedProcedure
  .input(z.object({ name: z.string() }))
  .mutation(async ({ input, ctx }) => {
    // ctx.user 已保证非 null
    return ctx.db.user.update({
      where: { id: ctx.user.id },
      data: { name: input.name },
    });
  });

Note

要点:ctx 类型收窄

next({ ctx: { ... } }) 的返回类型会沿链路累积。上面 isAutheduserUser | null 收窄为 User,下游 procedure 的 ctx.user 类型自动收窄。这是 tRPC 鉴权模式的常见用法。

日志 / 性能监控

const logger = middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  const durationMs = Date.now() - start;
  console.log(`[${type}] ${path} - ${durationMs}ms`);
  return result;
});

export const loggedProcedure = publicProcedure.use(logger);

Client

客户端通过 AppRouter 类型创建代理。两种常见形态:

纯 Client(无 React)

import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "../server/router";

export const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "http://localhost:3000/api/trpc",
    }),
  ],
});

// 使用
const user = await trpc.user.byId.query({ id: "1" });
const newUser = await trpc.user.create.mutate({
  name: "Alice",
  email: "a@b.c",
});

React + TanStack Query

// client.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../server/router";

export const trpc = createTRPCReact<AppRouter>();
// 组件里
const { data, isLoading } = trpc.user.byId.useQuery({ id: "1" });
const createUser = trpc.user.create.useMutation();

<button onClick={() => createUser.mutate({ name: "Alice", email: "a@b.c" })}>
  创建
</button>;

TanStack Query 的缓存、失效、乐观更新、无限滚动都能直接用。

httpBatchLink 会把同一个 tick 内的多个 procedure 调用自动合并为一个 HTTP 请求:

trpc.user.byId.query({ id: "1" })
trpc.user.byId.query({ id: "2" })
trpc.post.list.query()


  POST /api/trpc/user.byId,user.byId,post.list
  → 一次 HTTP 请求,服务端并行执行三个 procedure

Note

陷阱

  • Batching 让 DevTools Network 面板的请求 URL 形如 user.byId,user.byId,post.list,响应是数组,调试时不好读
  • 临时切到非批量的 httpLink 单独请求更好排查
  • 大量并发时如果某个 procedure 慢,会拖慢整批(必要时分成多个 httpBatchLink 实例)

Note

v11 新增 httpBatchStreamLink

tRPC v11 加入 httpBatchStreamLink,服务端用 HTTP chunked streaming 让每个 procedure 单独到达时立即返回,不用等整批最慢的完成。适合同一批里有快有慢的场景,API 与 httpBatchLink 一致,直接替换即可。

完整目录结构参考

一个 Next.js + tRPC 项目的典型组织:

src/
├── server/
│   ├── trpc.ts              # initTRPC + 导出 router / publicProcedure / middleware
│   ├── context.ts           # createContext + Context 类型
│   ├── routers/
│   │   ├── user.ts          # userRouter
│   │   ├── post.ts          # postRouter
│   │   └── _app.ts          # appRouter = router({ user, post, ... })
│   └── middlewares/
│       └── auth.ts          # isAuthed / protectedProcedure

├── app/api/trpc/[trpc]/
│   └── route.ts             # fetchRequestHandler 挂载

└── shared/
    └── trpc.ts              # createTRPCReact<AppRouter>()

回到 tRPC 总览