2.1 初始化 tRPC:initTRPC.create()
所有 tRPC 服务端代码都从 initTRPC.create() 开始。这个调用返回三个关键构建块:router、middleware、procedure(通常叫 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_REQUEST | 400 | 输入验证失败(Zod 自动使用) |
UNAUTHORIZED | 401 | 未登录 |
FORBIDDEN | 403 | 无权限 |
NOT_FOUND | 404 | 资源不存在 |
CONFLICT | 409 | 资源冲突(如邮箱已注册) |
TOO_MANY_REQUESTS | 429 | 限流 |
INTERNAL_SERVER_ERROR | 500 | 服务器内部错误 |
TIMEOUT | 408 | 请求超时 |
本章小结tRPC 服务端的核心是 initTRPC.create(),它返回 router、middleware、procedure 三个构建块。Procedure 分三类:.query() 读数据、.mutation() 写数据、.subscription() 实时推送。Router 可嵌套组织,根 Router 类型 AppRouter 是前后端类型共享的唯一桥梁。用 TRPCError 抛出语义化错误。