Chapter 06

Next.js App Router 集成

在 Next.js 15 中完整集成 tRPC,掌握服务端直调与客户端 Hooks 的协同

6.1 项目目录结构

标准的 Next.js 15 + tRPC 项目结构如下:

my-app/
├── app/
│   ├── api/
│   │   └── trpc/
│   │       └── [trpc]/
│   │           └── route.ts          # HTTP 端点
│   ├── layout.tsx
│   └── page.tsx
├── server/
│   ├── trpc.ts                        # initTRPC,publicProcedure
│   ├── context.ts                     # createContext
│   ├── router.ts                      # appRouter + AppRouter 类型
│   └── routers/
│       ├── user.ts
│       └── post.ts
├── lib/
│   ├── trpc.ts                        # createTRPCReact 客户端
│   └── trpc-server.ts                 # 服务端 caller(Server Components 用)
└── components/
    └── providers.tsx                   # TRPCProvider + QueryClientProvider
STRUCTURE

6.2 Route Handler 配置

Next.js App Router 使用 Route Handler(route.ts)而非 Pages Router 的 API Routes。tRPC 提供 fetchRequestHandler 适配器,兼容 Edge Runtime。

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/router';
import { createContext } from '@/server/context';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => createContext({ req }),
    // 生产环境屏蔽错误详情
    onError({ path, error }) {
      if (process.env.NODE_ENV === 'development') {
        console.error(`tRPC error on '${path}':`, error);
      }
    },
  });

// 同时处理 GET(Query)和 POST(Mutation)
export const { GET, POST } = { GET: handler, POST: handler };
TS

6.3 Server Component 中直接调用

在 Server Component 中,可以绕过 HTTP 层直接调用 Procedure。这利用了 tRPC 的 caller API,性能更高,且可以在服务端使用 Next.js 的 cache() 进行请求去重。

// lib/trpc-server.ts — 服务端 caller(Server Components 专用)
import { cache } from 'react';
import { createCallerFactory } from '@trpc/server';
import { appRouter } from '@/server/router';
import { createContext } from '@/server/context';
import { headers } from 'next/headers';

const createCaller = createCallerFactory(appRouter);

// React cache() 确保同一请求内多次调用只创建一次 Context
export const createServerCaller = cache(async () => {
  const heads = await headers();
  const ctx = await createContext({
    req: new Request('http://localhost', {
      headers: heads,
    }),
  });
  return createCaller(ctx);
});
TS
// app/posts/page.tsx — Server Component 直接调用
import { createServerCaller } from '@/lib/trpc-server';

export default async function PostsPage() {
  // 直接调用,无 HTTP 开销,支持 Next.js 数据缓存
  const caller = await createServerCaller();
  const posts = await caller.post.list({ page: 1 });
  // posts 类型完全推断

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
TSX

6.4 Hydration:服务端预取 + 客户端接管

为了兼顾 SEO(服务端渲染)和交互性(客户端 Hooks),tRPC 支持在服务端预取数据,然后在客户端 Hydrate。

// app/posts/page.tsx — 服务端预取
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { createServerCaller } from '@/lib/trpc-server';
import { getQueryKey } from '@trpc/react-query';
import { trpc } from '@/lib/trpc';
import { PostsClient } from './posts-client';

export default async function PostsPage() {
  const queryClient = new QueryClient();
  const caller = await createServerCaller();

  // 服务端预取,填充 queryClient 缓存
  await queryClient.prefetchQuery({
    queryKey: getQueryKey(trpc.post.list, { page: 1 }, 'query'),
    queryFn: () => caller.post.list({ page: 1 }),
  });

  return (
    {/* 将服务端缓存注入客户端 */}
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostsClient />
    </HydrationBoundary>
  );
}
TSX

6.5 Server Actions vs tRPC 选型

维度Server ActionstRPC
类型安全基本(需要手动推断)完整端到端推断
缓存不自动缓存TanStack Query 全套缓存
乐观更新useOptimistic(手动)TanStack Query 内置支持
错误处理需要手动 try/catch统一错误类型+onError
请求状态useFormStatus(有限)isPending/isLoading 等完整状态
复用性仅限 React 客户端可从任意客户端调用
学习曲线略高(需要学 tRPC + TanStack Query)
ℹ️

实践建议简单表单提交、页面级数据变更用 Server Actions(无需额外配置);复杂数据交互、需要乐观更新、多组件共享数据状态的场景用 tRPC。两者可以在同一项目中共存。

6.6 完整脚手架:create-t3-app

create-t3-app 是社区最流行的 Next.js + tRPC + Prisma + Auth.js 集成脚手架,开箱即用:

# 创建 T3 Stack 项目
pnpm create t3-app@latest

# 交互式选项:
# ✔ TypeScript? › Yes
# ✔ tRPC? › Yes
# ✔ Prisma? › Yes
# ✔ NextAuth.js? › Yes
# ✔ Tailwind CSS? › Yes
# ✔ App Router? › Yes
SHELL
# T3 App 的目录结构
src/
├── app/
│   ├── api/
│   │   ├── auth/[...nextauth]/route.ts
│   │   └── trpc/[trpc]/route.ts
│   ├── layout.tsx
│   └── page.tsx
├── server/
│   ├── api/
│   │   ├── root.ts               # appRouter
│   │   ├── trpc.ts               # initTRPC + procedures
│   │   └── routers/
│   │       └── post.ts
│   ├── auth.ts
│   └── db.ts
└── trpc/
    ├── query-client.ts
    ├── react.tsx                  # 客户端 Provider
    └── server.ts                  # 服务端 caller
STRUCTURE
💡

本章小结Next.js 15 + tRPC 的核心配置:① app/api/trpc/[trpc]/route.ts 使用 fetchRequestHandler 暴露 HTTP 端点;② Server Component 通过 createCallerFactory 直接调用 Procedure(零 HTTP 开销);③ 服务端预取 + HydrationBoundary 实现 SSR + 客户端 Hydration。对于快速启动,推荐使用 create-t3-app 脚手架。