全面掌握 tRPC:端到端类型安全的下一代 API 框架

12月 24, 2025
Frontend, ByAI, React, Nextjs

什么是 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-query

2. 基础项目结构#

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.json

3. 后端完整实现#

上下文配置(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,体验下一代全栈开发的极致效率!

本文共 3806 字,上次修改于 Dec 24, 2025,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» 全面掌握 TanStack Query:现代 React 应用的数据管理利器

» 理解 useMemo、useCallback 和 memo

» WebRTC 介绍

» pm2 使用

» Prisma 和 Drizzle 对比