Saltar al contenido principal
Versión: 11.x

Configuración con Next.js App Router

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

nota

Recomendamos leer la documentación de Advanced Server Rendering de TanStack React Query para comprender los diferentes tipos de renderizado en servidor y errores comunes a evitar.

Estructura de archivos recomendada

graphql
.
├── src
│ ├── app
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts # <-- tRPC HTTP handler
│ │ ├── layout.tsx # <-- mount TRPCReactProvider
│ │ └── page.tsx # <-- server component
│ ├── trpc
│ │ ├── init.ts # <-- tRPC server init & context
│ │ ├── routers
│ │ │ ├── _app.ts # <-- main app router
│ │ │ ├── post.ts # <-- sub routers
│ │ │ └── [..]
│ │ ├── client.tsx # <-- client hooks & provider
│ │ ├── query-client.ts # <-- shared QueryClient factory
│ │ └── server.tsx # <-- server-side caller
│ └── [..]
└── [..]
graphql
.
├── src
│ ├── app
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts # <-- tRPC HTTP handler
│ │ ├── layout.tsx # <-- mount TRPCReactProvider
│ │ └── page.tsx # <-- server component
│ ├── trpc
│ │ ├── init.ts # <-- tRPC server init & context
│ │ ├── routers
│ │ │ ├── _app.ts # <-- main app router
│ │ │ ├── post.ts # <-- sub routers
│ │ │ └── [..]
│ │ ├── client.tsx # <-- client hooks & provider
│ │ ├── query-client.ts # <-- shared QueryClient factory
│ │ └── server.tsx # <-- server-side caller
│ └── [..]
└── [..]

Agregar tRPC a un proyecto existente con Next.js App Router

1. Instalar dependencias

npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
Agentes de IA

Si utilizas un agente de IA para programación, instala habilidades de tRPC para una mejor generación de código:

bash
npx @tanstack/intent@latest install
bash
npx @tanstack/intent@latest install

2. Crear un router de tRPC

Inicializa tu backend tRPC en trpc/init.ts usando la función initTRPC, y crea tu primer router. Haremos un router simple "hola mundo" con un procedimiento básico. Para información detallada sobre cómo crear tu API tRPC, consulta la guía rápida y la documentación de uso del backend.

trpc/init.ts
ts
import { initTRPC } from '@trpc/server';
 
/**
* This context creator accepts `headers` so it can be reused in both
* the RSC server caller (where you pass `next/headers`) and the
* API route handler (where you pass the request headers).
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
// const user = await auth(opts.headers);
return { userId: 'user_123' };
};
 
// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC
.context<Awaited<ReturnType<typeof createTRPCContext>>>()
.create({
/**
* @see https://trpc.io/docs/server/data-transformers
*/
// transformer: superjson,
});
 
// Base router and procedure helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;
trpc/init.ts
ts
import { initTRPC } from '@trpc/server';
 
/**
* This context creator accepts `headers` so it can be reused in both
* the RSC server caller (where you pass `next/headers`) and the
* API route handler (where you pass the request headers).
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
// const user = await auth(opts.headers);
return { userId: 'user_123' };
};
 
// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC
.context<Awaited<ReturnType<typeof createTRPCContext>>>()
.create({
/**
* @see https://trpc.io/docs/server/data-transformers
*/
// transformer: superjson,
});
 
// Base router and procedure helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;

trpc/routers/_app.ts
ts
import { z } from 'zod';
import { baseProcedure, createTRPCRouter } from '../init';
 
export const appRouter = createTRPCRouter({
hello: baseProcedure
.input(
z.object({
text: z.string(),
}),
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
});
 
// export type definition of API
export type AppRouter = typeof appRouter;
trpc/routers/_app.ts
ts
import { z } from 'zod';
import { baseProcedure, createTRPCRouter } from '../init';
 
export const appRouter = createTRPCRouter({
hello: baseProcedure
.input(
z.object({
text: z.string(),
}),
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
});
 
// export type definition of API
export type AppRouter = typeof appRouter;

3. Crear el manejador de rutas API

Con App Router, usa el adaptador fetch para manejar solicitudes tRPC. Crea un manejador de ruta que exporte tanto GET como POST:

app/api/trpc/[trpc]/route.ts
ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createTRPCContext } from './trpc/init';
import { appRouter } from './trpc/routers/_app';
 
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext({ headers: req.headers }),
});
 
export { handler as GET, handler as POST };
app/api/trpc/[trpc]/route.ts
ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createTRPCContext } from './trpc/init';
import { appRouter } from './trpc/routers/_app';
 
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext({ headers: req.headers }),
});
 
export { handler as GET, handler as POST };
nota

App Router utiliza el adaptador fetch (vía fetchRequestHandler) en lugar del adaptador específico de Next.js usado en Pages Router. Esto se debe a que los manejadores de ruta de App Router se basan en los objetos estándar Request y Response de Web.

4. Crear una fábrica de Query Client

Crea un archivo compartido trpc/query-client.ts que exporte una función para crear instancias de QueryClient.

trpc/query-client.ts
ts
import {
defaultShouldDehydrateQuery,
QueryClient,
} from '@tanstack/react-query';
import superjson from 'superjson';
 
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
},
dehydrate: {
// serializeData: superjson.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
hydrate: {
// deserializeData: superjson.deserialize,
},
},
});
}
trpc/query-client.ts
ts
import {
defaultShouldDehydrateQuery,
QueryClient,
} from '@tanstack/react-query';
import superjson from 'superjson';
 
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
},
dehydrate: {
// serializeData: superjson.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
hydrate: {
// deserializeData: superjson.deserialize,
},
},
});
}

Establecemos algunas opciones predeterminadas:

  • staleTime: Con SSR, normalmente queremos establecer un staleTime predeterminado mayor a 0 para evitar recargas inmediatas en el cliente.

  • shouldDehydrateQuery: Función que determina si una query debe deshidratarse. Como el protocolo de transporte RSC admite hidratar promesas a través de la red, extendemos la función defaultShouldDehydrateQuery para incluir también queries que aún están pendientes. Esto nos permite iniciar prefetching en un componente servidor alto en el árbol, luego consumir esa promesa en un componente cliente más abajo.

  • serializeData y deserializeData (opcional): Si configuraste un transformador de datos en el paso anterior, establece esta opción para asegurar que los datos se serialicen correctamente al hidratar el cliente de queries en el límite servidor-cliente.

5. Crear un cliente tRPC para Componentes Cliente

trpc/client.tsx es el punto de entrada al consumir tu API tRPC desde componentes cliente. Aquí importa la definición de tipos de tu router tRPC y crea hooks tipados usando createTRPCContext. También exportaremos nuestro proveedor de contexto desde este archivo.

trpc/client.tsx
tsx
'use client';
 
// ^-- to make sure we can mount the Provider from a server component
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
 
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
 
let browserQueryClient: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient();
}
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
 
function getUrl() {
const base = (() => {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return 'http://localhost:3000';
})();
return `${base}/api/trpc`;
}
 
export function TRPCReactProvider(
props: Readonly<{
children: React.ReactNode;
}>,
) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
 
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
 
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}
trpc/client.tsx
tsx
'use client';
 
// ^-- to make sure we can mount the Provider from a server component
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
 
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
 
let browserQueryClient: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient();
}
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
 
function getUrl() {
const base = (() => {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return 'http://localhost:3000';
})();
return `${base}/api/trpc`;
}
 
export function TRPCReactProvider(
props: Readonly<{
children: React.ReactNode;
}>,
) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
 
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
 
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}

Monta el proveedor en el layout raíz de tu aplicación:

app/layout.tsx
tsx
import { TRPCReactProvider } from '~/trpc/client';
 
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}
app/layout.tsx
tsx
import { TRPCReactProvider } from '~/trpc/client';
 
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}

6. Crear un caller tRPC para Componentes Servidor

Para hacer prefetching de queries desde componentes servidor, creamos un proxy desde nuestro router. También puedes pasar un cliente si tu router está en un servidor separado.

trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
 
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { headers } from 'next/headers';
import { cache } from 'react';
import { createTRPCContext } from './init';
import { makeQueryClient } from './query-client';
import { appRouter } from './routers/_app';
 
// IMPORTANT: Create a stable getter for the query client that
// will return the same client during the same request.
export const getQueryClient = cache(makeQueryClient);
 
export const trpc = createTRPCOptionsProxy({
ctx: async () =>
createTRPCContext({
headers: await headers(),
}),
router: appRouter,
queryClient: getQueryClient,
});
 
// If your router is on a separate server, pass a client instead:
// createTRPCOptionsProxy({
// client: createTRPCClient({ links: [httpLink({ url: '...' })] }),
// queryClient: getQueryClient,
// });
trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
 
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { headers } from 'next/headers';
import { cache } from 'react';
import { createTRPCContext } from './init';
import { makeQueryClient } from './query-client';
import { appRouter } from './routers/_app';
 
// IMPORTANT: Create a stable getter for the query client that
// will return the same client during the same request.
export const getQueryClient = cache(makeQueryClient);
 
export const trpc = createTRPCOptionsProxy({
ctx: async () =>
createTRPCContext({
headers: await headers(),
}),
router: appRouter,
queryClient: getQueryClient,
});
 
// If your router is on a separate server, pass a client instead:
// createTRPCOptionsProxy({
// client: createTRPCClient({ links: [httpLink({ url: '...' })] }),
// queryClient: getQueryClient,
// });

7. Realizar solicitudes API

¡Todo listo! Ahora puedes hacer prefetching de queries en componentes servidor y consumirlas en componentes cliente.

Prefetching en un Componente Servidor

app/page.tsx
tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
void queryClient.prefetchQuery(
trpc.hello.queryOptions({
text: 'world',
}),
);
 
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ClientGreeting />
</HydrationBoundary>
);
}
app/page.tsx
tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
void queryClient.prefetchQuery(
trpc.hello.queryOptions({
text: 'world',
}),
);
 
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ClientGreeting />
</HydrationBoundary>
);
}

Usando datos en un Componente Cliente

app/client-greeting.tsx
tsx
'use client';
 
// <-- hooks can only be used in client components
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
 
// <-- hooks can only be used in client components
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}
consejo

Puedes crear funciones auxiliares prefetch y HydrateClient para hacer esto más conciso:

trpc/server.tsx
tsx
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{props.children}
</HydrationBoundary>
);
}
 
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
queryOptions: T,
) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === 'infinite') {
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(queryOptions);
}
}
trpc/server.tsx
tsx
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{props.children}
</HydrationBoundary>
);
}
 
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
queryOptions: T,
) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === 'infinite') {
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(queryOptions);
}
}

Luego puedes usarlo así:

tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}

Aprovechando Suspense

Puedes manejar estados de carga y error usando Suspense y Error Boundaries mediante el hook useSuspenseQuery.

app/page.tsx
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
prefetch(trpc.hello.queryOptions());
 
return (
<HydrateClient>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
app/page.tsx
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
prefetch(trpc.hello.queryOptions());
 
return (
<HydrateClient>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
app/client-greeting.tsx
tsx
'use client';
 
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.hello.queryOptions());
return <div>{data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
 
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.hello.queryOptions());
return <div>{data.greeting}</div>;
}

Obteniendo datos en un componente del servidor

Si necesitas acceder a datos en un componente del servidor, recomendamos crear un server caller y usarlo directamente. Nota que este método está desacoplado del query client y no almacena datos en caché. Esto significa que no puedes usar datos en un componente del servidor y esperarlos en el cliente. Es intencional y se explica en la guía Advanced Server Rendering.

trpc/server.tsx
tsx
import { headers } from 'next/headers';
import { createTRPCContext } from './init';
import { appRouter } from './routers/_app';
 
// ...
export const caller = appRouter.createCaller(async () =>
createTRPCContext({ headers: await headers() }),
);
trpc/server.tsx
tsx
import { headers } from 'next/headers';
import { createTRPCContext } from './init';
import { appRouter } from './routers/_app';
 
// ...
export const caller = appRouter.createCaller(async () =>
createTRPCContext({ headers: await headers() }),
);
app/page.tsx
tsx
import { caller } from '~/trpc/server';
 
export default async function Home() {
const greeting = await caller.hello();
const greeting: { greeting: string; }
 
return <div>{greeting.greeting}</div>;
}
app/page.tsx
tsx
import { caller } from '~/trpc/server';
 
export default async function Home() {
const greeting = await caller.hello();
const greeting: { greeting: string; }
 
return <div>{greeting.greeting}</div>;
}

Si realmente necesitas usar datos tanto en servidor como en cliente y entiendes las desventajas explicadas en la guía Advanced Server Rendering, usa fetchQuery en lugar de prefetch para tener datos en el servidor e hidratarlos en el cliente:

app/page.tsx
tsx
import { getQueryClient, HydrateClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());
 
// Do something with greeting on the server
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
app/page.tsx
tsx
import { getQueryClient, HydrateClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());
 
// Do something with greeting on the server
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}

Próximos pasos