全面掌握 tRPC:端到端类型安全的下一代 API 框架
12月 24, 2025
什么是 tRPC?#
tRPC 是一个颠覆性的远程过程调用框架,允许你在 TypeScript 中定义后端 API,并在前端直接调用这些 API 函数,同时享受完整的端到端类型安全。它不是一个替代 REST 或 GraphQL 的方案,而是一种全新的全栈开发范式。
核心价值主张#
🔒 端到端类型安全:后端修改 API,前端立即获得类型错误
🚀 零样板代码:无需手动定义 API 类型、DTO、序列化逻辑
⚡ 开发体验极致:IDE 自动补全、类型检查、重构安全
🔄 框架无关:支持 React、Next.js、Vue、Svelte 等
核心概念解析#
1. 路由器(Router) - API 的组织单元#
// 基础路由器结构
const appRouter = t.router({
// 查询操作(GET)
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
return { id: input.id, name: "John" };
}),
// 变更操作(POST/PUT/DELETE)
createUser: t.procedure
.input(z.object({ name: z.string(), email: z.string().email() }))
.mutation(({ input }) => {
return { id: "1", ...input, createdAt: new Date() };
}),
});2. 过程(Procedure) - API 端点#
每个过程包含:
输入验证:使用 Zod 进行运行时类型检查
业务逻辑:实际的函数实现
返回类型:自动推断的类型
完整项目实战指南#
1. 项目设置与安装#
# 后端依赖
npm install @trpc/server @trpc/client @trpc/next zod superjson
npm install -D @types/node typescript
# 前端依赖(Next.js)
npm install @trpc/react-query @tanstack/react-query2. 基础项目结构#
my-app/
├── src/
│ ├── server/
│ │ ├── trpc.ts # tRPC 上下文和工具函数
│ │ ├── routers/
│ │ │ ├── _app.ts # 根路由器
│ │ │ ├── user.ts # 用户相关路由
│ │ │ └── post.ts # 文章相关路由
│ │ └── context.ts # 请求上下文
│ ├── pages/
│ │ ├── api/
│ │ │ └── trpc/
│ │ │ └── [trpc].ts # tRPC HTTP 适配器
│ │ └── index.tsx # 前端页面
│ └── utils/
│ └── trpc.ts # 前端 tRPC 客户端配置
├── tsconfig.json
└── package.json3. 后端完整实现#
上下文配置(Context)#
// src/server/context.ts
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getSession } from 'next-auth/react';
// 为每个请求创建上下文
export async function createContext(opts: CreateNextContextOptions) {
const session = await getSession({ req: opts.req });
return {
session, // 认证信息
prisma, // 数据库客户端
req: opts.req, // HTTP 请求
res: opts.res, // HTTP 响应
user: session?.user, // 当前用户
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;tRPC 工具函数配置#
// src/server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { Context } from './context';
import superjson from 'superjson';
// 初始化 tRPC,配置上下文类型和数据处理
const t = initTRPC.context<Context>().create({
transformer: superjson, // 支持 Date/Map/Set 等特殊类型
errorFormatter: ({ shape, error }) => {
return {
...shape,
data: {
...shape.data,
// 添加自定义错误信息
zodError: error.code === 'BAD_REQUEST' && error.cause,
},
};
},
});
// 可复用的中间件
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: '请先登录'
});
}
return next({
ctx: {
...ctx,
user: ctx.session.user, // 确保 user 存在
},
});
});
// 导出可复用的工具函数
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);
export const middleware = t.middleware;用户路由器实现#
// src/server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { TRPCError } from '@trpc/server';
export const userRouter = router({
// 公开接口:获取用户信息
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
const user = await ctx.prisma.user.findUnique({
where: { id: input.id },
select: { id: true, name: true, email: true, image: true },
});
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: '用户不存在',
});
}
return user;
}),
// 受保护接口:更新用户资料
updateProfile: protectedProcedure
.input(z.object({
name: z.string().min(1).max(50).optional(),
email: z.string().email().optional(),
}))
.mutation(async ({ input, ctx }) => {
// 只有用户自己能更新自己的资料
if (ctx.user.id !== ctx.user.id) {
throw new TRPCError({
code: 'FORBIDDEN',
message: '无权操作',
});
}
const updatedUser = await ctx.prisma.user.update({
where: { id: ctx.user.id },
data: input,
});
return updatedUser;
}),
// 分页查询用户列表
list: publicProcedure
.input(z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(10),
search: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
const { page, limit, search } = input;
const skip = (page - 1) * limit;
const where = search ? {
OR: [
{ name: { contains: search } },
{ email: { contains: search } },
],
} : {};
const [users, totalCount] = await Promise.all([
ctx.prisma.user.findMany({
where,
skip,
take: limit,
select: { id: true, name: true, email: true },
orderBy: { createdAt: 'desc' },
}),
ctx.prisma.user.count({ where }),
]);
return {
users,
pagination: {
page,
limit,
totalCount,
totalPages: Math.ceil(totalCount / limit),
hasNext: page * limit < totalCount,
hasPrev: page > 1,
},
};
}),
});文章路由器实现(包含复杂关系)#
// src/server/routers/post.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
export const postRouter = router({
// 创建文章(包含标签关联)
create: protectedProcedure
.input(z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
tags: z.array(z.string()).max(5).optional(),
published: z.boolean().default(false),
}))
.mutation(async ({ input, ctx }) => {
const post = await ctx.prisma.post.create({
data: {
...input,
authorId: ctx.user.id,
// 处理标签关联
tags: input.tags ? {
connectOrCreate: input.tags.map(tag => ({
where: { name: tag },
create: { name: tag },
})),
} : undefined,
},
include: {
author: { select: { id: true, name: true } },
tags: true,
},
});
return post;
}),
// 无限滚动文章列表
infiniteScroll: publicProcedure
.input(z.object({
limit: z.number().min(1).max(50).default(10),
cursor: z.string().nullish(), // 用于分页的游标
}))
.query(async ({ input, ctx }) => {
const { limit, cursor } = input;
const posts = await ctx.prisma.post.findMany({
take: limit + 1, // 多取一条来判断是否有下一页
cursor: cursor ? { id: cursor } : undefined,
where: { published: true },
include: {
author: { select: { id: true, name: true } },
tags: true,
_count: { select: { likes: true, comments: true } },
},
orderBy: { createdAt: 'desc' },
});
let nextCursor: string | undefined = undefined;
if (posts.length > limit) {
const nextItem = posts.pop(); // 移除多余的一项
nextCursor = nextItem!.id;
}
return {
posts,
nextCursor,
};
}),
});根路由器聚合#
// src/server/routers/_app.ts
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
// 聚合所有路由器
export const appRouter = router({
user: userRouter,
post: postRouter,
// 可以添加健康检查等全局端点
healthcheck: router({
check: router.query(() => ({ status: 'ok', timestamp: new Date() })),
}),
});
// 导出类型,前端将使用这个类型
export type AppRouter = typeof appRouter;Next.js API 路由适配器#
// src/pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';
import { createContext } from '../../../server/context';
// 创建 tRPC API 处理器
export default createNextApiHandler({
router: appRouter,
createContext,
onError: ({ error, type, path, input, ctx, req }) => {
// 错误处理逻辑
console.error('tRPC 错误:', error);
// 生产环境不返回堆栈跟踪
if (process.env.NODE_ENV === 'production') {
error.message = '内部服务器错误';
}
},
// 启用响应缓存
responseMeta: ({ ctx, paths, type, errors }) => {
// 缓存公共查询
const allPublic = paths && paths.every(path => path.includes('public'));
const allOk = errors.length === 0;
if (allPublic && allOk) {
return {
headers: {
'cache-control': 's-maxage=60, stale-while-revalidate=300',
},
};
}
return {};
},
});4. 前端完整实现#
tRPC 客户端配置#
// src/utils/trpc.ts
import { createTRPCNext } from '@trpc/next';
import { httpBatchLink, loggerLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from '../server/routers/_app';
// 获取完整的 API URL
function getBaseUrl() {
if (typeof window !== 'undefined') return ''; // 浏览器端使用相对路径
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // Vercel
return `http://localhost:${process.env.PORT ?? 3000}`; // 开发环境
}
// 创建 tRPC 客户端
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
transformer: superjson,
links: [
// 日志链接(开发环境)
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error),
}),
// 批处理链接
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
// 允许传递请求头(如认证信息)
headers() {
if (ctx?.req?.headers) {
// 服务端:转发所有头部
return { ...ctx.req.headers };
}
// 客户端:从存储中获取 token
const token = localStorage.getItem('token');
return token ? { authorization: `Bearer ${token}` } : {};
},
}),
],
// 查询配置
queryClientConfig: {
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5分钟
retry: (failureCount, error: any) => {
// 认证错误不重试
if (error?.data?.code === 'UNAUTHORIZED') return false;
return failureCount < 3;
},
},
},
},
};
},
// 服务端渲染时是否预取数据
ssr: true,
});React 组件集成#
// src/pages/index.tsx
import { trpc } from '../utils/trpc';
import { useState } from 'react';
export default function HomePage() {
// 状态管理
const [search, setSearch] = useState('');
const [currentPage, setCurrentPage] = useState(1);
// tRPC 查询 - 自动类型安全!
const { data: users, isLoading, error } = trpc.user.list.useQuery({
page: currentPage,
limit: 10,
search: search || undefined,
});
// tRPC 变更 - 自动类型安全!
const createUserMutation = trpc.user.create.useMutation();
// 无限滚动查询
const {
data: postsData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = trpc.post.infiniteScroll.useInfiniteQuery(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
// 处理创建用户
const handleCreateUser = async (userData: { name: string; email: string }) => {
try {
const newUser = await createUserMutation.mutateAsync(userData);
console.log('用户创建成功:', newUser);
// 可以在这里触发重新获取用户列表
} catch (error) {
console.error('创建用户失败:', error);
}
};
if (error) {
return <div>错误: {error.message}</div>;
}
return (
<div>
<h1>用户管理</h1>
{/* 搜索框 */}
<input
type="text"
placeholder="搜索用户..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{/* 用户列表 */}
{isLoading ? (
<div>加载中...</div>
) : (
<div>
{users?.users.map((user) => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
{/* 分页控件 */}
<div>
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage(prev => prev - 1)}
>
上一页
</button>
<span>第 {currentPage} 页,共 {users?.pagination.totalPages} 页</span>
<button
disabled={!users?.pagination.hasNext}
onClick={() => setCurrentPage(prev => prev + 1)}
>
下一页
</button>
</div>
</div>
)}
{/* 无限滚动文章列表 */}
<div>
<h2>最新文章</h2>
{postsData?.pages.map((page, pageIndex) => (
<div key={pageIndex}>
{page.posts.map((post) => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>作者: {post.author.name}</p>
<p>喜欢数: {post._count.likes}</p>
</article>
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
</div>
</div>
);
}5. 高级特性与疑难解答#
自定义错误处理#
// 扩展 tRPC 错误类型
export class BusinessError extends Error {
constructor(public code: string, message: string) {
super(message);
this.name = 'BusinessError';
}
}
// 错误处理中间件
const errorHandlingMiddleware = middleware(async ({ next, ctx, path, type }) => {
try {
return await next({ ctx });
} catch (error) {
// 记录错误
console.error(`tRPC 错误 [${type}] ${path}:`, error);
// 转换业务错误
if (error instanceof BusinessError) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: error.message,
cause: error,
});
}
// 重新抛出其他错误
throw error;
}
});
// 应用中间件
export const procedureWithErrorHandling = t.procedure.use(errorHandlingMiddleware);文件上传处理#
// 文件上传路由器
export const fileRouter = router({
upload: protectedProcedure
.input(z.object({
file: z.any(), // 实际项目中可以使用更精确的类型
fileName: z.string(),
}))
.mutation(async ({ input, ctx }) => {
// 这里可以集成云存储服务
const fileUrl = await uploadToCloudStorage(input.file, input.fileName);
return { url: fileUrl };
}),
});
// 前端上传组件
function FileUpload() {
const [file, setFile] = useState<File | null>(null);
const uploadMutation = trpc.file.upload.useMutation();
const handleUpload = async () => {
if (!file) return;
// 创建 FormData 或直接传递文件
await uploadMutation.mutateAsync({
file: file,
fileName: file.name,
});
};
return <input type="file" onChange={(e) => setFile(e.target.files?.[0] || null)} />;
}WebSocket 实时通信#
// 实时路由器(使用 tRPC 的订阅功能)
export const realtimeRouter = router({
onUpdate: publicProcedure
.input(z.object({ roomId: z.string() }))
.subscription(({ input }) => {
return observable<{ message: string; data: any }>((emit) => {
const onUpdate = (data: any) => {
emit.next({ message: 'updated', data });
};
// 订阅外部事件源
eventEmitter.on(`update:${input.roomId}`, onUpdate);
// 清理函数
return () => {
eventEmitter.off(`update:${input.roomId}`, onUpdate);
};
});
}),
});常见疑难问题与解决方案#
1. 循环依赖问题#
// ❌ 错误:路由器间循环导入
// user.ts 导入 post.ts,post.ts 又导入 user.ts
// ✅ 正确:使用依赖注入模式
export const createUserRouter = (dependencies: { postRouter: any }) =>
router({ /* ... */ });
// 或者在根路由器中合并
export const appRouter = router({
user: userRouter,
post: postRouter,
});2. 类型导出与导入#
// ❌ 错误:直接导出类型可能导致问题
export type { AppRouter };
// ✅ 正确:使用 TypeScript 的类型导出
export type AppRouter = typeof appRouter;
// 前端使用
import type { AppRouter } from '../server/routers/_app';3. 中间件执行顺序#
// 中间件按顺序执行
const procedure = t.procedure
.use(loggingMiddleware) // 1. 日志
.use(authMiddleware) // 2. 认证
.use(validationMiddleware) // 3. 验证
.use(cachingMiddleware); // 4. 缓存
4. 错误处理最佳实践#
// 统一的错误处理
const unifiedErrorHandler = middleware(async ({ next, ctx }) => {
try {
return await next({ ctx });
} catch (error) {
// 记录到日志系统
logger.error('tRPC Error', error);
// 生产环境隐藏内部错误详情
if (process.env.NODE_ENV === 'production') {
if (error instanceof TRPCError) {
throw error; // 已知错误直接抛出
}
// 未知错误统一处理
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: '服务器内部错误',
});
}
throw error; // 开发环境显示详细错误
}
});性能优化技巧#
1. 查询去重与缓存#
// 使用 React Query 的缓存策略
const trpc = createTRPCNext<AppRouter>({
config() {
return {
queryClientConfig: {
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟
cacheTime: 1000 * 60 * 10, // 10分钟
},
},
},
};
},
});2. 批量请求优化#
// tRPC 自动批量处理并发请求
const [user, posts, settings] = await Promise.all([
trpc.user.getById.query({ id: '1' }),
trpc.post.list.query({}),
trpc.settings.get.query({}),
]);测试策略#
单元测试示例#
import { createCaller } from '../server/routers/_app';
import { createMockContext } from './test-utils';
describe('User Router', () => {
it('应该能获取用户信息', async () => {
const ctx = createMockContext();
const caller = createCaller(ctx);
const user = await caller.user.getById({ id: '1' });
expect(user.id).toBe('1');
expect(user.name).toBe('John');
});
});部署与生产环境配置#
环境变量配置#
// 生产环境配置
const trpcConfig = {
// 启用更严格的错误处理
isDev: process.env.NODE_ENV === 'development',
// 配置 CORS
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
credentials: true,
},
// 响应缓存配置
responseCache: {
// 缓存公共查询 1 小时
public: { maxAge: 60 * 60, swr: 60 * 10 },
},
};总结#
tRPC 通过端到端的类型安全,彻底改变了全栈开发的体验。它消除了传统 API 开发中的类型同步问题,让开发者能够专注于业务逻辑而不是类型定义。
关键优势:
🎯 类型安全:编译时捕获 API 错误
⚡ 开发效率:减少 50% 以上的样板代码
🔧 开发者体验:完美的 IDE 支持
🚀 性能:自动批处理、缓存优化
适用场景:
TypeScript 全栈项目
需要快速迭代的创业项目
对类型安全有高要求的商业应用
团队协作开发项目
开始使用 tRPC,体验下一代全栈开发的极致效率!