tRPCSeniorSystem design

Как использовать tRPC с Next.js (App Router и Pages Router)?

В Pages Router используется createNextApiHandler для API-route и createTRPCNext для React-хуков. В App Router серверные компоненты вызывают процедуры напрямую через createCallerFactory, клиентские компоненты — через createTRPCReact с fetchRequestHandler.

tRPC в Next.js Pages Router

Классический подход: API Route в pages/api/trpc/[trpc].ts обрабатывает все запросы через fetchRequestHandler или createNextApiHandler.

// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/trpc/router';
import { createContext } from '../../../server/trpc/context';

export default createNextApiHandler({
  router: appRouter,
  createContext,
  onError({ error, path }) {
    if (error.code === 'INTERNAL_SERVER_ERROR') {
      console.error(`Error on ${path}:`, error);
    }
  },
});
// utils/trpc.ts (Pages Router клиент)
import { createTRPCNext } from '@trpc/next';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from '../server/trpc/router';

export const trpc = createTRPCNext<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: '/api/trpc',
          transformer: superjson,
        }),
      ],
    };
  },
  ssr: false, // или true для SSR с prefetch
});
// pages/_app.tsx
import { trpc } from '../utils/trpc';

export default trpc.withTRPC(MyApp);

tRPC в Next.js App Router

App Router различает серверные и клиентские компоненты. Подход разный для каждого случая.

API Route Handler (app/api/trpc/[trpc]/route.ts)

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

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => createContext(req),
  });

export { handler as GET, handler as POST };

Серверные компоненты — прямые вызовы без HTTP

// app/posts/page.tsx (Server Component)
import { createCallerFactory } from '@trpc/server';
import { appRouter } from '../../server/trpc/router';
import { createContext } from '../../server/trpc/context';
import { headers } from 'next/headers';

const createCaller = createCallerFactory(appRouter);

export default async function PostsPage() {
  // Прямой вызов без HTTP-запроса
  const ctx = await createContext(new Request('http://internal', {
    headers: await headers(),
  }));
  const caller = createCaller(ctx);
  const posts = await caller.post.list({ limit: 10 });

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

Клиентские компоненты в App Router

// lib/trpc/client.ts
'use client';
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../server/trpc/router';

export const trpc = createTRPCReact<AppRouter>();
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import { useState } from 'react';
import { trpc } from '../lib/trpc/client';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc',
          transformer: superjson,
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

Подводные камни

  • В App Router нельзя использовать createTRPCNext (он для Pages Router) — это частая ошибка при миграции.
  • Файл с createTRPCReact должен иметь директиву 'use client' вверху, иначе Next.js выбросит ошибку о использовании клиентских API в Server Component.
  • Серверные компоненты не должны импортировать клиентский trpc-файл — создавайте отдельные файлы для серверного (createCallerFactory) и клиентского (createTRPCReact) кода.
  • При SSR в Pages Router с ssr: true данные сериализуются через dehydrate/hydrate — при использовании superjson нужно настроить трансформер и для этого слоя.
  • Route Handler в App Router (route.ts) должен экспортировать и GET, и POST — tRPC использует GET для queries и POST для mutations/batch.
  • createCallerFactory в Server Components не проходит через HTTP-middleware (CORS, rate limiting) — авторизацию нужно проверять в контексте явно.
  • Providers-компонент с QueryClient должен создавать новый экземпляр через useState, а не на уровне модуля — иначе данные будут шариться между запросами на сервере.

Common mistakes

  • Смешивать «tRPC с Next.js» с похожим механизмом без критерия выбора.
  • Игнорировать риск: неверно оценить границы применения темы «tRPC с Next.js» и получить хрупкое решение.
  • Показывать только синтаксис и не объяснять поведение в runtime или сборке.

What the interviewer is testing

  • Объясняет интеграция с App Router, Pages Router, SSR и server components.
  • Показывает на примере, как работает: в Pages Router используют next adapter и SSR helpers, а в App Router важны route handlers, server/client component boundary и создание QueryClient без утечки между requests.
  • Называет production-нюанс и граничный случай для темы «tRPC с Next.js».

Sources

Related topics