tRPC 路由

什么是 tRPC?

tRPC 是一个让你在 TypeScript 全栈应用中实现端到端类型安全 API 的框架。与 REST 或 GraphQL 不同,tRPC 不需要代码生成、schema 定义或序列化层 — 你在服务端定义函数,客户端自动获得完整的类型推断。这意味着当你修改 API 签名时,TypeScript 编译器会立即在客户端报错,从根本上消除了 API 层的运行时类型错误。

tRPC 特别适合 Next.js、Remix 等 TypeScript 全栈框架,但也可以和 Express、Fastify 等任何 Node.js 服务端框架配合使用。

快速开始

安装依赖

npm install @trpc/server @trpc/client @trpc/react-query zod
npm install @tanstack/react-query

初始化 tRPC

// server/trpc.ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';

const t = initTRPC.context<Context>().create({
  transformer: superjson,
});

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

创建第一个路由

// server/routers/greeting.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const greetingRouter = router({
  hello: publicProcedure
    .input(z.object({ name: z.string().optional() }))
    .query(({ input }) => {
      return { message: `Hello ${input.name ?? 'world'}!` };
    }),
});

导出 AppRouter 类型

// server/root.ts
import { router } from './trpc';
import { greetingRouter } from './routers/greeting';

export const appRouter = router({
  greeting: greetingRouter,
});

// 仅导出类型 — 不导出运行时代码
export type AppRouter = typeof appRouter;

核心概念

Procedures(过程)

Procedure 是 tRPC 中暴露给客户端的函数。有三种类型:query(读取数据)、mutation(写入数据)、subscription(实时数据推送)。

Query — 读取数据

// 查询用户列表,支持分页
userList: publicProcedure
  .input(z.object({
    limit: z.number().min(1).max(100).default(20),
    cursor: z.string().nullish(),
  }))
  .query(async ({ input, ctx }) => {
    const items = await ctx.db.user.findMany({
      take: input.limit + 1,
      cursor: input.cursor ? { id: input.cursor } : undefined,
      orderBy: { createdAt: 'desc' },
    });
    let nextCursor: string | undefined;
    if (items.length > input.limit) {
      nextCursor = items.pop()!.id;
    }
    return { items, nextCursor };
  }),

Mutation — 写入数据

// 创建新用户
createUser: protectedProcedure
  .input(z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    role: z.enum(['USER', 'ADMIN']).default('USER'),
  }))
  .mutation(async ({ input, ctx }) => {
    const user = await ctx.db.user.create({ data: input });
    return user;
  }),

Subscription — 实时数据

import { observable } from '@trpc/server/observable';

onNewMessage: publicProcedure
  .input(z.object({ channelId: z.string() }))
  .subscription(({ input }) => {
    return observable<Message>((emit) => {
      const handler = (msg: Message) => emit.next(msg);
      messageEmitter.on(`channel:${input.channelId}`, handler);
      return () => {
        messageEmitter.off(`channel:${input.channelId}`, handler);
      };
    });
  }),

Zod 输入验证

tRPC 与 Zod 深度集成,所有输入都在运行时验证并自动推断类型。

基本 schema

const createPostInput = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1),
  published: z.boolean().default(false),
});

嵌套对象

const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zip: z.string().regex(/^\d{5,6}$/),
  country: z.string().length(2),
});

const userInput = z.object({
  name: z.string(),
  address: addressSchema,
  billingAddress: addressSchema.optional(),
});

数组验证

const batchInput = z.object({
  ids: z.array(z.string().uuid()).min(1).max(50),
  tags: z.array(z.string()).max(10).default([]),
});

组合输入(.input().input())

// 多次调用 .input() 会自动合并 schema
updatePost: protectedProcedure
  .input(z.object({ id: z.string() }))
  .input(z.object({ title: z.string(), content: z.string() }))
  .mutation(({ input }) => {
    // input 类型: { id: string; title: string; content: string }
  }),

Context(上下文)

Context 是每个请求共享的对象,通常包含数据库连接、当前用户会话等。它在中间件和 procedure 中通过 ctx 访问。

创建 Context

// server/context.ts
import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getServerSession } from 'next-auth';
import { prisma } from './db';

export async function createContext({ req, res }: CreateNextContextOptions) {
  const session = await getServerSession(req, res, authOptions);
  return {
    db: prisma,
    session,
    req,
    res,
  };
}

export type Context = inferAsyncReturnType<typeof createContext>;

在 Procedure 中访问 Context

getProfile: protectedProcedure.query(({ ctx }) => {
  // ctx.session.user 在 protectedProcedure 中确保存在
  return ctx.db.user.findUnique({
    where: { id: ctx.session.user.id },
    select: { id: true, name: true, email: true, avatar: true },
  });
}),

中间件

中间件可以在 procedure 执行前后运行逻辑,用于认证检查、日志记录、性能监控等。

认证中间件(Protected Procedure)

const isAuthed = middleware(({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      ...ctx,
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
});

export const protectedProcedure = publicProcedure.use(isAuthed);

日志中间件

const logger = middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  const duration = Date.now() - start;
  if (result.ok) {
    console.log(`[${type}] ${path} - ${duration}ms OK`);
  } else {
    console.error(`[${type}] ${path} - ${duration}ms ERROR`);
  }
  return result;
});

耗时监控中间件

const timing = middleware(async ({ path, next }) => {
  const start = performance.now();
  const result = await next();
  const elapsed = performance.now() - start;
  if (elapsed > 1000) {
    console.warn(`Slow procedure: ${path} took ${elapsed.toFixed(0)}ms`);
  }
  return result;
});

链式组合中间件

// 中间件按 .use() 调用顺序依次执行
export const adminProcedure = publicProcedure
  .use(logger)
  .use(timing)
  .use(isAuthed)
  .use(isAdmin);  // 每个中间件可以扩展/收窄 ctx 类型

路由组织

嵌套路由

// server/routers/user.ts
export const userRouter = router({
  list: publicProcedure.query(/* ... */),
  byId: publicProcedure.input(z.string()).query(/* ... */),
  create: protectedProcedure.input(createUserSchema).mutation(/* ... */),
  update: protectedProcedure.input(updateUserSchema).mutation(/* ... */),
  delete: adminProcedure.input(z.string()).mutation(/* ... */),
});

// server/routers/post.ts
export const postRouter = router({
  list: publicProcedure.query(/* ... */),
  bySlug: publicProcedure.input(z.string()).query(/* ... */),
  create: protectedProcedure.input(createPostSchema).mutation(/* ... */),
});

合并路由

// server/root.ts
import { router } from './trpc';
import { userRouter } from './routers/user';
import { postRouter } from './routers/post';
import { commentRouter } from './routers/comment';

export const appRouter = router({
  user: userRouter,
  post: postRouter,
  comment: commentRouter,
});

export type AppRouter = typeof appRouter;

推荐的文件结构

src/ server/ trpc.ts # initTRPC, middleware, procedure 导出 context.ts # createContext 函数 root.ts # appRouter, AppRouter 类型 routers/ user.ts # 用户相关 procedures post.ts # 文章相关 procedures comment.ts # 评论相关 procedures admin.ts # 管理员 procedures utils/ trpc.ts # 客户端 trpc hooks (createTRPCReact)

框架集成

框架适配器包名
Next.js Pages RoutercreateNextApiHandler@trpc/server/adapters/next
Next.js App RouterfetchRequestHandler@trpc/server/adapters/fetch
ExpresscreateExpressMiddleware@trpc/server/adapters/express
FastifyfastifyTRPCPlugin@trpc/server/adapters/fastify
Standalone (Node)createHTTPServer@trpc/server/adapters/standalone

Next.js Pages Router

// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '~/server/root';
import { createContext } from '~/server/context';

export default createNextApiHandler({
  router: appRouter,
  createContext,
  onError: ({ error, path }) => {
    if (error.code === 'INTERNAL_SERVER_ERROR') {
      console.error(`tRPC error on ${path}:`, error);
    }
  },
});

Next.js App Router

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

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

export { handler as GET, handler as POST };

Express 适配器

import express from 'express';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { appRouter } from './root';
import { createContext } from './context';

const app = express();

app.use(
  '/api/trpc',
  createExpressMiddleware({ router: appRouter, createContext })
);

app.listen(3000);

Fastify 适配器

import Fastify from 'fastify';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import { appRouter } from './root';
import { createContext } from './context';

const server = Fastify();

server.register(fastifyTRPCPlugin, {
  prefix: '/api/trpc',
  trpcOptions: { router: appRouter, createContext },
});

server.listen({ port: 3000 });

错误处理

TRPCError 错误码

错误码HTTP 状态使用场景
BAD_REQUEST400输入数据无效
UNAUTHORIZED401未登录
FORBIDDEN403权限不足
NOT_FOUND404资源不存在
CONFLICT409资源冲突(如重复创建)
TOO_MANY_REQUESTS429请求频率超限
INTERNAL_SERVER_ERROR500服务器内部错误
TIMEOUT408请求超时

抛出 TRPCError

import { TRPCError } from '@trpc/server';

getPost: publicProcedure
  .input(z.object({ id: z.string() }))
  .query(async ({ input, ctx }) => {
    const post = await ctx.db.post.findUnique({ where: { id: input.id } });
    if (!post) {
      throw new TRPCError({
        code: 'NOT_FOUND',
        message: `Post with id "${input.id}" not found`,
      });
    }
    return post;
  }),

自定义错误格式

import { ZodError } from 'zod';

const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError
            ? error.cause.flatten()
            : null,
      },
    };
  },
});

客户端错误处理

// React Query 风格
const { data, error } = trpc.post.byId.useQuery({ id: '123' });

if (error) {
  if (error.data?.code === 'NOT_FOUND') {
    return <NotFound />;
  }
  // Zod 验证错误
  if (error.data?.zodError) {
    const fieldErrors = error.data.zodError.fieldErrors;
    // 渲染字段级错误...
  }
}

高级特性

Output Transformer(superjson)

默认 JSON 不支持 Date、Map、Set 等类型。使用 superjson 可以自动序列化/反序列化这些类型。

import superjson from 'superjson';

// 服务端
const t = initTRPC.context<Context>().create({
  transformer: superjson,
});

// 客户端也需要配置相同的 transformer
import { createTRPCClient, httpBatchLink } from '@trpc/client';
const client = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: '/api/trpc', transformer: superjson })],
});

Procedure 元数据

type Meta = { requiresAuth: boolean; rateLimit?: number };

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

// 使用元数据
publicProcedure
  .meta({ requiresAuth: false, rateLimit: 100 })
  .query(() => { /* ... */ });

// 在中间件中读取
const rateLimiter = middleware(async ({ meta, next }) => {
  if (meta?.rateLimit) {
    // 执行限流逻辑...
  }
  return next();
});

批量请求

// 客户端默认使用 httpBatchLink
// 同一 tick 内的多个请求会自动合并为一个 HTTP 请求
import { httpBatchLink } from '@trpc/client';

const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: '/api/trpc',
      maxURLLength: 2083, // GET 请求 URL 长度限制
    }),
  ],
});

服务端直接调用(Server-Side Caller)

// 在 Server Components 或后端逻辑中直接调用 procedure
const caller = appRouter.createCaller({
  session: null,
  db: prisma,
});

// 不走 HTTP,直接执行
const users = await caller.user.list({ limit: 10 });
const post = await caller.post.bySlug('hello-world');

Links(splitLink 条件路由)

import { splitLink, httpBatchLink, httpLink } from '@trpc/client';

const client = createTRPCClient<AppRouter>({
  links: [
    splitLink({
      condition: (op) => op.type === 'subscription',
      true: wsLink({ url: 'ws://localhost:3001' }),
      false: httpBatchLink({ url: '/api/trpc' }),
    }),
  ],
});

常见问题 (FAQ)

tRPC vs REST vs GraphQL?

tRPC 适合 TypeScript 全栈项目,提供零成本的端到端类型安全。REST 适合公开 API 和多语言客户端。GraphQL 适合复杂数据图和需要精确字段选择的场景。如果前后端都用 TypeScript,tRPC 是开发效率最高的选择。

不用 Next.js 可以吗?

当然可以。tRPC 提供 Express、Fastify、Standalone HTTP、Fetch API 等多种适配器。任何 Node.js 服务端框架都可以使用 tRPC,客户端也可以用 vanilla JS 或 React Query 以外的方案。

如何处理文件上传?

tRPC 不原生支持 multipart/form-data。推荐方案:(1) 使用单独的 REST 端点处理上传,返回 URL 后通过 tRPC mutation 保存元数据;(2) 使用 presigned URL 直传到 S3/R2,再通过 tRPC 记录文件信息。

如何添加速率限制?

在中间件层实现。可以使用 Procedure 的 meta 字段标记限流配置,在中间件中读取 meta 并调用 Redis 或内存限流器。也可以在 Express/Fastify 的适配器层面统一添加 express-rate-limit 等中间件。

如何测试 tRPC procedures?

使用 appRouter.createCaller() 创建服务端调用者,传入模拟的 context(如测试数据库连接、假 session),然后像调用普通函数一样测试每个 procedure。不需要启动 HTTP 服务器。

代码速查