Chapter 02

服务端基础——Router 与 Procedure

从 initTRPC 出发,掌握三种 Procedure 类型和模块化 Router 设计

2.1 初始化 tRPC:initTRPC.create()

所有 tRPC 服务端代码都从 initTRPC.create() 开始。这个调用返回三个关键构建块:routermiddlewareprocedure(通常叫 publicProcedure)。

⚠️

初始化规则initTRPC.create() 每个应用只能调用一次。将初始化代码放在独立文件(如 server/trpc.ts)并从中导出,其他文件从这里导入,不要重复调用 initTRPC

// server/trpc.ts — tRPC 初始化,全项目唯一
import { initTRPC } from '@trpc/server';
import { Context } from './context'; // 第4章详细讲

/**
 * 初始化 tRPC,传入 Context 类型参数
 * Context 包含用户信息、数据库连接等请求级别的状态
 */
const t = initTRPC.context<Context>().create();

// 导出 router 构建函数
export const router = t.router;

// 导出 middleware 构建函数
export const middleware = t.middleware;

// 导出基础 Procedure(未经任何认证保护)
export const publicProcedure = t.procedure;
TS

initTRPC 配置选项

const t = initTRPC.context<Context>().create({
  // 自定义错误格式化(生产环境可屏蔽堆栈信息)
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError: error.cause instanceof ZodError
          ? error.cause.flatten()
          : null,
      },
    };
  },

  // 数据转换器(支持 Date、Map、Set 等非 JSON 类型)
  transformer: superjson,
});
TS

2.2 Query Procedure:读操作

Query 对应 HTTP GET 请求,用于读取数据,不产生副作用。Query 的结果可以被客户端缓存。定义 Query 使用 .query(handler),handler 接收 { input, ctx } 并返回数据。

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

export const postRouter = router({
  // 获取文章列表
  list: publicProcedure
    .input(z.object({
      page: z.number().min(1).default(1),
      pageSize: z.number().max(100).default(20),
    }))
    .query(async ({ input }) => {
      const { page, pageSize } = input;
      const posts = await db.post.findMany({
        skip: (page - 1) * pageSize,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
      });
      return posts; // 返回类型自动推断
    }),

  // 获取单篇文章
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const post = await db.post.findUnique({
        where: { id: input.id },
      });
      if (!post) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: '文章不存在',
        });
      }
      return post;
    }),
});
TS

2.3 Mutation Procedure:写操作

Mutation 对应 HTTP POST 请求,用于创建、更新、删除等有副作用的操作。定义 Mutation 使用 .mutation(handler)

export const postRouter = router({
  // ...前面的 Query...

  // 创建文章
  create: publicProcedure
    .input(z.object({
      title: z.string().min(1).max(200),
      content: z.string().min(10),
      published: z.boolean().default(false),
    }))
    .mutation(async ({ input, ctx }) => {
      return await db.post.create({
        data: {
          ...input,
          authorId: ctx.user.id,
        },
      });
    }),

  // 更新文章
  update: publicProcedure
    .input(z.object({
      id: z.string(),
      title: z.string().optional(),
      content: z.string().optional(),
    }))
    .mutation(async ({ input }) => {
      const { id, ...data } = input;
      return await db.post.update({
        where: { id },
        data,
      });
    }),

  // 删除文章
  delete: publicProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ input }) => {
      await db.post.delete({ where: { id: input.id } });
      return { success: true };
    }),
});
TS

2.4 Subscription Procedure:实时订阅

Subscription 基于 AsyncGenerator 实现服务器推送,需要配合 WebSocket 使用。与 Query/Mutation 不同,Subscription 保持持久连接,服务端可以主动推送数据。

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

export const notificationRouter = router({
  // 订阅实时通知
  onNotification: publicProcedure
    .input(z.object({ userId: z.string() }))
    .subscription(({ input }) => {
      // 返回一个 Observable,每次 emit 都会推送给客户端
      return observable<{ message: string; type: string }>((emit) => {
        // 订阅内部事件总线
        const onNotify = (notification: Notification) => {
          if (notification.userId === input.userId) {
            emit.next({ message: notification.message, type: notification.type });
          }
        };

        eventEmitter.on('notification', onNotify);

        // 清理函数:客户端断开时调用
        return () => {
          eventEmitter.off('notification', onNotify);
        };
      });
    }),
});
TS

2.5 Router 定义与模块化合并

随着项目增长,将所有 Procedure 写在一个文件会变得难以维护。tRPC 支持将 Router 按领域拆分,然后合并成一个根 Router。

// server/routers/user.ts
export const userRouter = router({
  getMe: publicProcedure.query(...)
  updateProfile: publicProcedure.mutation(...)
});

// server/routers/post.ts
export const postRouter = router({
  list: publicProcedure.query(...)
  create: publicProcedure.mutation(...)
});

// server/router.ts — 根 Router
import { router } from './trpc';
import { userRouter } from './routers/user';
import { postRouter } from './routers/post';

export const appRouter = router({
  user: userRouter,   // 访问:trpc.user.getMe
  post: postRouter,   // 访问:trpc.post.list
});

// 导出根 Router 的类型——前端只需要这个
export type AppRouter = typeof appRouter;
TS

mergeRouters:扁平化合并

除了嵌套 Router,tRPC 还提供 mergeRouters 将多个 Router 扁平合并(所有 Procedure 在同一层级):

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

export const appRouter = mergeRouters(userRouter, postRouter);
// 访问:trpc.getMe(不需要 user. 前缀)
// 注意:键名冲突时后面的 Router 会覆盖前面的
TS

2.6 AppRouter 类型导出

AppRouter 是整个 tRPC 类型系统的核心。前端通过 import type 导入这个类型,从而获得完整的端到端类型推断。tRPC 还提供了一些实用类型工具:

import type { AppRouter } from '../server/router';
import { inferRouterInputs, inferRouterOutputs } from '@trpc/server';

// 推断所有路由的输入类型
type RouterInput = inferRouterInputs<AppRouter>;
type CreatePostInput = RouterInput['post']['create'];
// ^ { title: string; content: string; published?: boolean }

// 推断所有路由的返回类型
type RouterOutput = inferRouterOutputs<AppRouter>;
type PostItem = RouterOutput['post']['list'][0];
// ^ 自动推断出 Post 对象的完整类型
TS

2.7 错误处理:TRPCError

在 Procedure 中抛出 TRPCError 来返回特定的错误状态。tRPC 会将其转换为对应的 HTTP 状态码,客户端也会获得类型安全的错误对象。

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

throw new TRPCError({
  code: 'NOT_FOUND',       // HTTP 404
  message: '资源不存在',
  cause: originalError,    // 可选:包装原始错误
});
TS
错误码HTTP 状态使用场景
BAD_REQUEST400输入验证失败(Zod 自动使用)
UNAUTHORIZED401未登录
FORBIDDEN403无权限
NOT_FOUND404资源不存在
CONFLICT409资源冲突(如邮箱已注册)
TOO_MANY_REQUESTS429限流
INTERNAL_SERVER_ERROR500服务器内部错误
TIMEOUT408请求超时
💡

本章小结tRPC 服务端的核心是 initTRPC.create(),它返回 routermiddlewareprocedure 三个构建块。Procedure 分三类:.query() 读数据、.mutation() 写数据、.subscription() 实时推送。Router 可嵌套组织,根 Router 类型 AppRouter 是前后端类型共享的唯一桥梁。用 TRPCError 抛出语义化错误。