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