Set up with Next.js App Router
We recommend reading TanStack React Query's Advanced Server Rendering docs to understand the different types of server rendering and footguns to avoid.
Recommended file structure
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│ └── [..]└── [..]
Add tRPC to an existing Next.js App Router project
1. Install deps
- npm
- yarn
- pnpm
- bun
- deno
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
yarn add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
pnpm add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
bun add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
deno add npm:@trpc/server npm:@trpc/client npm:@trpc/tanstack-react-query npm:@tanstack/react-query@latest npm:zod npm:client-only npm:server-only
2. Create a tRPC router
Initialize your tRPC backend in trpc/init.ts using the initTRPC function, and create your first router. We're going to make a simple "hello world" router and procedure here — for deeper information on creating your tRPC API, refer to the Quickstart guide and Backend usage docs.
trpc/init.tstsimport {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 constcreateTRPCContext = 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.constt =initTRPC .context <Awaited <ReturnType <typeofcreateTRPCContext >>>().create ({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport constcreateTRPCRouter =t .router ;export constcreateCallerFactory =t .createCallerFactory ;export constbaseProcedure =t .procedure ;
trpc/init.tstsimport {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 constcreateTRPCContext = 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.constt =initTRPC .context <Awaited <ReturnType <typeofcreateTRPCContext >>>().create ({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport constcreateTRPCRouter =t .router ;export constcreateCallerFactory =t .createCallerFactory ;export constbaseProcedure =t .procedure ;
trpc/routers/_app.tstsimport {z } from 'zod';import {baseProcedure ,createTRPCRouter } from '../init';export constappRouter =createTRPCRouter ({hello :baseProcedure .input (z .object ({text :z .string (),}),).query ((opts ) => {return {greeting : `hello ${opts .input .text }`,};}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
trpc/routers/_app.tstsimport {z } from 'zod';import {baseProcedure ,createTRPCRouter } from '../init';export constappRouter =createTRPCRouter ({hello :baseProcedure .input (z .object ({text :z .string (),}),).query ((opts ) => {return {greeting : `hello ${opts .input .text }`,};}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
3. Create the API route handler
With the App Router, use the fetch adapter to handle tRPC requests. Create a route handler that exports both GET and POST:
app/api/trpc/[trpc]/route.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createTRPCContext } from './trpc/init';import {appRouter } from './trpc/routers/_app';consthandler = (req :Request ) =>fetchRequestHandler ({endpoint : '/api/trpc',req ,router :appRouter ,createContext : () =>createTRPCContext ({headers :req .headers }),});export {handler asGET ,handler asPOST };
app/api/trpc/[trpc]/route.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createTRPCContext } from './trpc/init';import {appRouter } from './trpc/routers/_app';consthandler = (req :Request ) =>fetchRequestHandler ({endpoint : '/api/trpc',req ,router :appRouter ,createContext : () =>createTRPCContext ({headers :req .headers }),});export {handler asGET ,handler asPOST };
App Router uses the fetch adapter (via fetchRequestHandler) rather than the Next.js-specific adapter used by the Pages Router. This is because App Router route handlers are based on the Web standard Request and Response objects.
4. Create a Query Client factory
Create a shared file trpc/query-client.ts that exports a function that creates a QueryClient instance.
trpc/query-client.tstsimport {defaultShouldDehydrateQuery ,QueryClient ,} from '@tanstack/react-query';importsuperjson from 'superjson';export functionmakeQueryClient () {return newQueryClient ({defaultOptions : {queries : {staleTime : 30 * 1000,},dehydrate : {// serializeData: superjson.serialize,shouldDehydrateQuery : (query ) =>defaultShouldDehydrateQuery (query ) ||query .state .status === 'pending',},hydrate : {// deserializeData: superjson.deserialize,},},});}
trpc/query-client.tstsimport {defaultShouldDehydrateQuery ,QueryClient ,} from '@tanstack/react-query';importsuperjson from 'superjson';export functionmakeQueryClient () {return newQueryClient ({defaultOptions : {queries : {staleTime : 30 * 1000,},dehydrate : {// serializeData: superjson.serialize,shouldDehydrateQuery : (query ) =>defaultShouldDehydrateQuery (query ) ||query .state .status === 'pending',},hydrate : {// deserializeData: superjson.deserialize,},},});}
We're setting a few default options here:
staleTime: With SSR, we usually want to set some default staleTime above 0 to avoid refetching immediately on the client.shouldDehydrateQuery: This is a function that determines whether a query should be dehydrated or not. Since the RSC transport protocol supports hydrating promises over the network, we extend thedefaultShouldDehydrateQueryfunction to also include queries that are still pending. This will allow us to start prefetching in a server component high up the tree, then consuming that promise in a client component further down.serializeDataanddeserializeData(optional): If you set up a data transformer in the previous step, set this option to make sure the data is serialized correctly when hydrating the query client over the server-client boundary.
5. Create a tRPC client for Client Components
The trpc/client.tsx is the entrypoint when consuming your tRPC API from client components. In here, import the type definition of
your tRPC router and create typesafe hooks using createTRPCContext. We'll also export our context provider from this file.
trpc/client.tsxtsx'use client';// ^-- to make sure we can mount the Provider from a server componentimport 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 >();letbrowserQueryClient :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// 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 clientif (!browserQueryClient )browserQueryClient =makeQueryClient ();returnbrowserQueryClient ;}functiongetUrl () {constbase = (() => {if (typeofwindow !== 'undefined') return '';if (process .env .VERCEL_URL ) return `https://${process .env .VERCEL_URL }`;return 'http://localhost:3000';})();return `${base }/api/trpc`;}export functionTRPCReactProvider (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 boundaryconstqueryClient =getQueryClient ();const [trpcClient ] =useState (() =>createTRPCClient <AppRouter >({links : [httpBatchLink ({// transformer: superjson, <-- if you use a data transformerurl :getUrl (),}),],}),);return (<QueryClientProvider client ={queryClient }><TRPCProvider trpcClient ={trpcClient }queryClient ={queryClient }>{props .children }</TRPCProvider ></QueryClientProvider >);}
trpc/client.tsxtsx'use client';// ^-- to make sure we can mount the Provider from a server componentimport 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 >();letbrowserQueryClient :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// 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 clientif (!browserQueryClient )browserQueryClient =makeQueryClient ();returnbrowserQueryClient ;}functiongetUrl () {constbase = (() => {if (typeofwindow !== 'undefined') return '';if (process .env .VERCEL_URL ) return `https://${process .env .VERCEL_URL }`;return 'http://localhost:3000';})();return `${base }/api/trpc`;}export functionTRPCReactProvider (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 boundaryconstqueryClient =getQueryClient ();const [trpcClient ] =useState (() =>createTRPCClient <AppRouter >({links : [httpBatchLink ({// transformer: superjson, <-- if you use a data transformerurl :getUrl (),}),],}),);return (<QueryClientProvider client ={queryClient }><TRPCProvider trpcClient ={trpcClient }queryClient ={queryClient }>{props .children }</TRPCProvider ></QueryClientProvider >);}
Mount the provider in the root layout of your application:
app/layout.tsxtsximport {TRPCReactProvider } from '~/trpc/client';export default functionRootLayout ({children ,}:Readonly <{children :React .ReactNode ;}>) {return (<html lang ="en"><body ><TRPCReactProvider >{children }</TRPCReactProvider ></body ></html >);}
app/layout.tsxtsximport {TRPCReactProvider } from '~/trpc/client';export default functionRootLayout ({children ,}:Readonly <{children :React .ReactNode ;}>) {return (<html lang ="en"><body ><TRPCReactProvider >{children }</TRPCReactProvider ></body ></html >);}
6. Create a tRPC caller for Server Components
To prefetch queries from server components, we create a proxy from our router. You can also pass in a client if your router is on a separate server.
trpc/server.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport {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 constgetQueryClient =cache (makeQueryClient );export consttrpc =createTRPCOptionsProxy ({ctx : async () =>createTRPCContext ({headers : awaitheaders (),}),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.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport {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 constgetQueryClient =cache (makeQueryClient );export consttrpc =createTRPCOptionsProxy ({ctx : async () =>createTRPCContext ({headers : awaitheaders (),}),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. Make API requests
You're all set! You can now prefetch queries in server components and consume them in client components.
Prefetching in a Server Component
app/page.tsxtsximport {dehydrate ,HydrationBoundary } from '@tanstack/react-query';import {getQueryClient ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {constqueryClient =getQueryClient ();voidqueryClient .prefetchQuery (trpc .hello .queryOptions ({text : 'world',}),);return (<HydrationBoundary state ={dehydrate (queryClient )}><ClientGreeting /></HydrationBoundary >);}
app/page.tsxtsximport {dehydrate ,HydrationBoundary } from '@tanstack/react-query';import {getQueryClient ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {constqueryClient =getQueryClient ();voidqueryClient .prefetchQuery (trpc .hello .queryOptions ({text : 'world',}),);return (<HydrationBoundary state ={dehydrate (queryClient )}><ClientGreeting /></HydrationBoundary >);}
Using data in a Client Component
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport {useQuery } from '@tanstack/react-query';import {useTRPC } from '~/trpc/client';export functionClientGreeting () {consttrpc =useTRPC ();constgreeting =useQuery (trpc .hello .queryOptions ({text : 'world' }));if (!greeting .data ) return <div >Loading...</div >;return <div >{greeting .data .greeting }</div >;}
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport {useQuery } from '@tanstack/react-query';import {useTRPC } from '~/trpc/client';export functionClientGreeting () {consttrpc =useTRPC ();constgreeting =useQuery (trpc .hello .queryOptions ({text : 'world' }));if (!greeting .data ) return <div >Loading...</div >;return <div >{greeting .data .greeting }</div >;}
You can create prefetch and HydrateClient helper functions to make this more concise:
trpc/server.tsxtsxexport functionHydrateClient (props : {children :React .ReactNode }) {constqueryClient =getQueryClient ();return (<HydrationBoundary state ={dehydrate (queryClient )}>{props .children }</HydrationBoundary >);}export functionprefetch <T extendsReturnType <TRPCQueryOptions <any>>>(queryOptions :T ,) {constqueryClient =getQueryClient ();if (queryOptions .queryKey [1]?.type === 'infinite') {voidqueryClient .prefetchInfiniteQuery (queryOptions as any);} else {voidqueryClient .prefetchQuery (queryOptions );}}
trpc/server.tsxtsxexport functionHydrateClient (props : {children :React .ReactNode }) {constqueryClient =getQueryClient ();return (<HydrationBoundary state ={dehydrate (queryClient )}>{props .children }</HydrationBoundary >);}export functionprefetch <T extendsReturnType <TRPCQueryOptions <any>>>(queryOptions :T ,) {constqueryClient =getQueryClient ();if (queryOptions .queryKey [1]?.type === 'infinite') {voidqueryClient .prefetchInfiniteQuery (queryOptions as any);} else {voidqueryClient .prefetchQuery (queryOptions );}}
Then you can use it like this:
tsximport {HydrateClient ,prefetch ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';functionHome () {prefetch (trpc .hello .queryOptions ({text : 'world' }));return (<HydrateClient ><ClientGreeting /></HydrateClient >);}
tsximport {HydrateClient ,prefetch ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';functionHome () {prefetch (trpc .hello .queryOptions ({text : 'world' }));return (<HydrateClient ><ClientGreeting /></HydrateClient >);}
Leveraging Suspense
You may prefer handling loading and error states using Suspense and Error Boundaries. You can do this by using the useSuspenseQuery hook.
app/page.tsxtsximport {HydrateClient ,prefetch ,trpc } from '~/trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {prefetch (trpc .hello .queryOptions ());return (<HydrateClient ><ErrorBoundary fallback ={<div >Something went wrong</div >}><Suspense fallback ={<div >Loading...</div >}><ClientGreeting /></Suspense ></ErrorBoundary ></HydrateClient >);}
app/page.tsxtsximport {HydrateClient ,prefetch ,trpc } from '~/trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {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.tsxtsx'use client';import {useSuspenseQuery } from '@tanstack/react-query';import {useTRPC } from '~/trpc/client';export functionClientGreeting () {consttrpc =useTRPC ();const {data } =useSuspenseQuery (trpc .hello .queryOptions ());return <div >{data .greeting }</div >;}
app/client-greeting.tsxtsx'use client';import {useSuspenseQuery } from '@tanstack/react-query';import {useTRPC } from '~/trpc/client';export functionClientGreeting () {consttrpc =useTRPC ();const {data } =useSuspenseQuery (trpc .hello .queryOptions ());return <div >{data .greeting }</div >;}
Getting data in a Server Component
If you need access to the data in a server component, we recommend creating a server caller and using it directly. Please note that this method is detached from your query client and does not store the data in the cache. This means that you cannot use the data in a server component and expect it to be available in the client. This is intentional and explained in more detail in the Advanced Server Rendering guide.
trpc/server.tsxtsximport {headers } from 'next/headers';import {createTRPCContext } from './init';import {appRouter } from './routers/_app';// ...export constcaller =appRouter .createCaller (async () =>createTRPCContext ({headers : awaitheaders () }),);
trpc/server.tsxtsximport {headers } from 'next/headers';import {createTRPCContext } from './init';import {appRouter } from './routers/_app';// ...export constcaller =appRouter .createCaller (async () =>createTRPCContext ({headers : awaitheaders () }),);
app/page.tsxtsximport {caller } from '~/trpc/server';export default async functionHome () {constgreeting = awaitcaller .hello ();return <div >{greeting .greeting }</div >;}
app/page.tsxtsximport {caller } from '~/trpc/server';export default async functionHome () {constgreeting = awaitcaller .hello ();return <div >{greeting .greeting }</div >;}
If you really need to use the data both on the server as well as inside client components and understand the tradeoffs explained in the
Advanced Server Rendering
guide, you can use fetchQuery instead of prefetch to have the data both on the server as well as hydrating it down to the client:
app/page.tsxtsximport {getQueryClient ,HydrateClient ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {constqueryClient =getQueryClient ();constgreeting = awaitqueryClient .fetchQuery (trpc .hello .queryOptions ());// Do something with greeting on the serverreturn (<HydrateClient ><ClientGreeting /></HydrateClient >);}
app/page.tsxtsximport {getQueryClient ,HydrateClient ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {constqueryClient =getQueryClient ();constgreeting = awaitqueryClient .fetchQuery (trpc .hello .queryOptions ());// Do something with greeting on the serverreturn (<HydrateClient ><ClientGreeting /></HydrateClient >);}
Next steps
- Learn about Server Actions for defining tRPC-powered server actions
- Learn about queries and mutations in client components
- Explore server-side calls for more advanced server-side patterns
- Check out the SSE Chat example for a full App Router example with subscriptions