tRPC 路由器与过程 #

核心概念概览 #

tRPC 的核心由两个主要概念组成:Router(路由器)和 Procedure(过程)。理解这两个概念是掌握 tRPC 的关键。

text
┌─────────────────────────────────────────────────────────────┐
│                    tRPC 架构概览                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   App Router (应用路由器)                                    │
│   ├── User Router (用户路由器)                              │
│   │   ├── list (Procedure - Query)                         │
│   │   ├── getById (Procedure - Query)                      │
│   │   └── create (Procedure - Mutation)                    │
│   │                                                         │
│   ├── Post Router (文章路由器)                              │
│   │   ├── list (Procedure - Query)                         │
│   │   ├── create (Procedure - Mutation)                    │
│   │   └── onNew (Procedure - Subscription)                 │
│   │                                                         │
│   └── Comment Router (评论路由器)                           │
│       ├── list (Procedure - Query)                         │
│       └── create (Procedure - Mutation)                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Router(路由器) #

什么是 Router? #

Router 是 tRPC 中组织 API 端点的方式。它类似于命名空间,可以将相关的过程分组在一起。

typescript
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

const userRouter = t.router({
  list: t.procedure.query(() => { /* ... */ }),
  getById: t.procedure.query(() => { /* ... */ }),
  create: t.procedure.mutation(() => { /* ... */ }),
});

const appRouter = t.router({
  user: userRouter,
});

创建 Router #

基本创建 #

typescript
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

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

const appRouter = router({
  hello: publicProcedure.query(() => {
    return { message: 'Hello World!' };
  }),
});

export type AppRouter = typeof appRouter;

嵌套 Router #

typescript
const userRouter = router({
  list: publicProcedure.query(() => { /* ... */ }),
  getById: publicProcedure.query(() => { /* ... */ }),
});

const postRouter = router({
  list: publicProcedure.query(() => { /* ... */ }),
  create: publicProcedure.mutation(() => { /* ... */ }),
});

const appRouter = router({
  user: userRouter,
  post: postRouter,
});
text
┌─────────────────────────────────────────────────────────────┐
│                    嵌套路由结构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  客户端调用方式:                                            │
│                                                             │
│  userRouter:                                                │
│  - client.user.list.query()                                │
│  - client.user.getById.query({ id: 1 })                    │
│                                                             │
│  postRouter:                                                │
│  - client.post.list.query()                                │
│  - client.post.create.mutate({ title: '...' })             │
│                                                             │
│  路径映射:                                                  │
│  user.list     -> /trpc/user.list                          │
│  post.create   -> /trpc/post.create                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

合并多个 Router #

typescript
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;

Router 最佳实践 #

按功能模块拆分 #

text
src/server/routers/
├── index.ts        # 合并所有路由
├── user.ts         # 用户相关
├── post.ts         # 文章相关
├── comment.ts      # 评论相关
├── auth.ts         # 认证相关
└── admin.ts        # 管理相关

路由文件示例 #

typescript
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { z } from 'zod';
import { db } from '../db';

export const userRouter = router({
  list: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).optional(),
      cursor: z.string().optional(),
    }))
    .query(async ({ input }) => {
      const limit = input.limit ?? 10;
      const users = await db.user.findMany({
        take: limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
      });
      
      let nextCursor: string | undefined = undefined;
      if (users.length > limit) {
        const nextItem = users.pop();
        nextCursor = nextItem!.id;
      }
      
      return { users, nextCursor };
    }),

  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return db.user.findUnique({
        where: { id: input.id },
      });
    }),

  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }))
    .mutation(async ({ input, ctx }) => {
      return db.user.create({
        data: {
          ...input,
          createdBy: ctx.user.id,
        },
      });
    }),

  update: protectedProcedure
    .input(z.object({
      id: z.string(),
      name: z.string().min(1).optional(),
      email: z.string().email().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      return db.user.update({
        where: { id: input.id },
        data: input,
      });
    }),

  delete: protectedProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ input, ctx }) => {
      await db.user.delete({
        where: { id: input.id },
      });
      return { success: true };
    }),
});

Procedure(过程) #

什么是 Procedure? #

Procedure 是 tRPC 中的 API 端点,类似于 REST API 中的路由。每个 Procedure 可以是以下三种类型之一:

text
┌─────────────────────────────────────────────────────────────┐
│                    Procedure 类型                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Query(查询)                                               │
│  ├── 用于获取数据                                           │
│  ├── 类似 REST GET                                          │
│  ├── 不应该修改数据                                          │
│  └── 可以缓存                                               │
│                                                             │
│  Mutation(变更)                                            │
│  ├── 用于修改数据                                           │
│  ├── 类似 REST POST/PUT/DELETE                              │
│  ├── 有副作用                                               │
│  └── 不应该缓存                                              │
│                                                             │
│  Subscription(订阅)                                        │
│  ├── 用于实时数据                                           │
│  ├── 长连接                                                 │
│  ├── 服务端推送                                              │
│  └── 需要 WebSocket                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Query(查询) #

Query 用于获取数据,不应该有副作用。

typescript
const router = t.router({
  user: t.router({
    list: t.procedure
      .input(z.object({
        limit: z.number().optional(),
        offset: z.number().optional(),
      }))
      .query(async ({ input }) => {
        const { limit = 10, offset = 0 } = input;
        return db.user.findMany({
          take: limit,
          skip: offset,
        });
      }),

    getById: t.procedure
      .input(z.object({ id: z.string() }))
      .query(async ({ input }) => {
        return db.user.findUnique({
          where: { id: input.id },
        });
      }),

    search: t.procedure
      .input(z.object({
        query: z.string(),
        fields: z.array(z.string()).optional(),
      }))
      .query(async ({ input }) => {
        return db.user.findMany({
          where: {
            OR: [
              { name: { contains: input.query } },
              { email: { contains: input.query } },
            ],
          },
        });
      }),
  }),
});

Mutation(变更) #

Mutation 用于修改数据,有副作用。

typescript
const router = t.router({
  user: t.router({
    create: t.procedure
      .input(z.object({
        name: z.string().min(1, 'Name is required'),
        email: z.string().email('Invalid email'),
        password: z.string().min(8, 'Password must be at least 8 characters'),
      }))
      .mutation(async ({ input }) => {
        const hashedPassword = await hashPassword(input.password);
        return db.user.create({
          data: {
            name: input.name,
            email: input.email,
            password: hashedPassword,
          },
        });
      }),

    update: t.procedure
      .input(z.object({
        id: z.string(),
        data: z.object({
          name: z.string().min(1).optional(),
          email: z.string().email().optional(),
        }),
      }))
      .mutation(async ({ input }) => {
        return db.user.update({
          where: { id: input.id },
          data: input.data,
        });
      }),

    delete: t.procedure
      .input(z.object({ id: z.string() }))
      .mutation(async ({ input }) => {
        await db.user.delete({
          where: { id: input.id },
        });
        return { success: true };
      }),
  }),
});

Subscription(订阅) #

Subscription 用于实时数据推送。

typescript
import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';

const ee = new EventEmitter();

const router = t.router({
  post: t.router({
    onNew: t.procedure
      .input(z.object({
        channelId: z.string(),
      }))
      .subscription(({ input }) => {
        return observable<{ id: string; title: string }>((emit) => {
          const onNew = (post: { id: string; title: string }) => {
            emit.next(post);
          };
          
          ee.on('newPost', onNew);
          
          return () => {
            ee.off('newPost', onNew);
          };
        });
      }),

    create: t.procedure
      .input(z.object({
        title: z.string(),
        content: z.string(),
      }))
      .mutation(async ({ input }) => {
        const post = await db.post.create({
          data: input,
        });
        
        ee.emit('newPost', post);
        
        return post;
      }),
  }),
});
text
┌─────────────────────────────────────────────────────────────┐
│                    Subscription 工作流程                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   客户端                          服务端                     │
│   ┌─────────┐                    ┌─────────┐               │
│   │         │  1. 建立连接       │         │               │
│   │         │ ─────────────────> │         │               │
│   │         │                    │         │               │
│   │         │  2. 订阅事件       │         │               │
│   │         │ ─────────────────> │  EventEmitter│           │
│   │         │                    │         │               │
│   │         │  3. 推送数据       │         │               │
│   │         │ <───────────────── │         │               │
│   │         │                    │         │               │
│   │         │  4. 取消订阅       │         │               │
│   │         │ ─────────────────> │         │               │
│   └─────────┘                    └─────────┘               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Input 验证 #

使用 Zod #

tRPC 推荐使用 Zod 进行输入验证:

typescript
import { z } from 'zod';

const router = t.router({
  user: t.router({
    create: t.procedure
      .input(z.object({
        name: z.string().min(1, 'Name is required'),
        email: z.string().email('Invalid email format'),
        age: z.number().min(0).max(150).optional(),
        role: z.enum(['user', 'admin']).default('user'),
        preferences: z.object({
          theme: z.enum(['light', 'dark']).optional(),
          notifications: z.boolean().optional(),
        }).optional(),
      }))
      .mutation(async ({ input }) => {
        return db.user.create({ data: input });
      }),
  }),
});

常用 Zod 验证 #

typescript
import { z } from 'zod';

const schemas = {
  id: z.string().uuid(),
  email: z.string().email(),
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Must contain uppercase')
    .regex(/[a-z]/, 'Must contain lowercase')
    .regex(/[0-9]/, 'Must contain number'),
  
  pagination: z.object({
    limit: z.number().min(1).max(100).default(10),
    offset: z.number().min(0).default(0),
    cursor: z.string().optional(),
  }),
  
  dateRange: z.object({
    start: z.date(),
    end: z.date(),
  }).refine(
    (data) => data.end > data.start,
    { message: 'End date must be after start date' }
  ),
  
  search: z.object({
    query: z.string().min(1),
    filters: z.array(z.string()).optional(),
    sort: z.enum(['asc', 'desc']).optional(),
  }),
};

复用 Schema #

typescript
import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

const router = t.router({
  user: t.router({
    create: t.procedure
      .input(userSchema)
      .mutation(async ({ input }) => {
        return db.user.create({ data: input });
      }),

    update: t.procedure
      .input(z.object({
        id: z.string(),
        data: userSchema.partial(),
      }))
      .mutation(async ({ input }) => {
        return db.user.update({
          where: { id: input.id },
          data: input.data,
        });
      }),
  }),
});

Output 类型 #

自动推断 #

tRPC 会自动推断输出类型:

typescript
const router = t.router({
  user: t.router({
    getById: t.procedure
      .input(z.object({ id: z.string() }))
      .query(async ({ input }) => {
        return db.user.findUnique({
          where: { id: input.id },
          select: {
            id: true,
            name: true,
            email: true,
          },
        });
      }),
  }),
});

type Output = {
  id: string;
  name: string;
  email: string;
} | null;

显式定义 Output #

typescript
import { z } from 'zod';

const userOutputSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  createdAt: z.date(),
});

const router = t.router({
  user: t.router({
    getById: t.procedure
      .input(z.object({ id: z.string() }))
      .output(userOutputSchema)
      .query(async ({ input }) => {
        const user = await db.user.findUnique({
          where: { id: input.id },
        });
        
        if (!user) {
          throw new TRPCError({
            code: 'NOT_FOUND',
            message: 'User not found',
          });
        }
        
        return user;
      }),
  }),
});

Output 验证 #

typescript
const router = t.router({
  user: t.router({
    list: t.procedure
      .output(z.array(z.object({
        id: z.string(),
        name: z.string(),
        email: z.string().optional(),
      })))
      .query(async () => {
        return db.user.findMany();
      }),
  }),
});

Meta 元数据 #

定义 Meta #

typescript
import { initTRPC } from '@trpc/server';

interface Meta {
  requiresAuth: boolean;
  role?: 'user' | 'admin';
}

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

const router = t.router({
  admin: t.router({
    deleteUser: t.procedure
      .meta({ requiresAuth: true, role: 'admin' })
      .input(z.object({ id: z.string() }))
      .mutation(async ({ input }) => {
        return db.user.delete({ where: { id: input.id } });
      }),
  }),
});

使用 Meta #

typescript
const isAuthed = t.middleware(({ ctx, meta, next }) => {
  if (meta?.requiresAuth && !ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }

  if (meta?.role && ctx.user?.role !== meta.role) {
    throw new TRPCError({ code: 'FORBIDDEN' });
  }

  return next();
});

export const protectedProcedure = t.procedure.use(isAuthed);

过程链(Procedure Chaining) #

链式调用 #

typescript
const router = t.router({
  user: t.router({
    create: t.procedure
      .use(loggingMiddleware)
      .use(authMiddleware)
      .use(rateLimitMiddleware)
      .input(createUserSchema)
      .output(userOutputSchema)
      .mutation(async ({ input, ctx }) => {
        return createUser(input, ctx);
      }),
  }),
});

复用过程配置 #

typescript
const baseProcedure = t.procedure
  .use(loggingMiddleware)
  .use(timingMiddleware);

const protectedProcedure = baseProcedure
  .use(authMiddleware);

const adminProcedure = protectedProcedure
  .use(adminMiddleware);

const router = t.router({
  public: t.router({
    list: baseProcedure.query(() => { /* ... */ }),
  }),
  
  user: t.router({
    update: protectedProcedure.mutation(() => { /* ... */ }),
  }),
  
  admin: t.router({
    deleteUser: adminProcedure.mutation(() => { /* ... */ }),
  }),
});
text
┌─────────────────────────────────────────────────────────────┐
│                    过程链结构                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  baseProcedure                                              │
│  └── loggingMiddleware                                      │
│      └── timingMiddleware                                   │
│                                                             │
│  protectedProcedure                                         │
│  └── baseProcedure                                          │
│      └── authMiddleware                                     │
│                                                             │
│  adminProcedure                                             │
│  └── protectedProcedure                                     │
│      └── adminMiddleware                                    │
│                                                             │
│  调用顺序:                                                  │
│  logging -> timing -> auth -> admin -> handler              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

完整示例 #

用户路由 #

typescript
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { TRPCError } from '@trpc/server';

const createUserSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password too short'),
});

const updateUserSchema = z.object({
  id: z.string(),
  name: z.string().min(1).optional(),
  email: z.string().email().optional(),
});

const userOutputSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  createdAt: z.date(),
  updatedAt: z.date(),
});

export const userRouter = router({
  list: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(10),
      cursor: z.string().optional(),
    }))
    .output(z.object({
      items: z.array(userOutputSchema),
      nextCursor: z.string().optional(),
    }))
    .query(async ({ input }) => {
      const items = await 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) {
        items.pop();
        nextCursor = items[items.length - 1]?.id;
      }

      return { items, nextCursor };
    }),

  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .output(userOutputSchema.nullable())
    .query(async ({ input }) => {
      return db.user.findUnique({
        where: { id: input.id },
      });
    }),

  create: publicProcedure
    .input(createUserSchema)
    .output(userOutputSchema)
    .mutation(async ({ input }) => {
      const existing = await db.user.findUnique({
        where: { email: input.email },
      });

      if (existing) {
        throw new TRPCError({
          code: 'CONFLICT',
          message: 'Email already exists',
        });
      }

      const hashedPassword = await hash(input.password, 10);
      
      return db.user.create({
        data: {
          name: input.name,
          email: input.email,
          password: hashedPassword,
        },
      });
    }),

  update: protectedProcedure
    .input(updateUserSchema)
    .output(userOutputSchema)
    .mutation(async ({ input, ctx }) => {
      const user = await db.user.findUnique({
        where: { id: input.id },
      });

      if (!user) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'User not found',
        });
      }

      if (user.id !== ctx.user.id) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Not authorized',
        });
      }

      return db.user.update({
        where: { id: input.id },
        data: input,
      });
    }),

  delete: protectedProcedure
    .input(z.object({ id: z.string() }))
    .output(z.object({ success: z.boolean() }))
    .mutation(async ({ input, ctx }) => {
      if (ctx.user.id !== input.id) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Not authorized',
        });
      }

      await db.user.delete({
        where: { id: input.id },
      });

      return { success: true };
    }),
});

最佳实践 #

1. 路由组织 #

text
✅ 按功能模块拆分路由
✅ 使用清晰的命名
✅ 保持路由扁平化
✅ 合并相关操作

❌ 过深的嵌套
❌ 过大的路由文件
❌ 模糊的命名

2. 过程设计 #

text
✅ 单一职责
✅ 清晰的输入输出
✅ 适当的验证
✅ 错误处理

❌ 过于复杂的过程
❌ 缺少验证
❌ 忽略错误处理

3. 类型安全 #

text
✅ 使用 Zod 验证
✅ 导出类型
✅ 复用 Schema
✅ 显式输出类型

❌ 使用 any
❌ 跳过验证
❌ 类型重复定义

下一步 #

现在你已经掌握了 tRPC 的路由器和过程,接下来学习 客户端使用,了解如何在各种前端框架中使用 tRPC!

最后更新:2026-03-29