Skip to content
tRPC

Partners

Set up with Next.js Pages Router

On this page

The Pages Router uses the same client as the App Router: the TanStack React Query integration (@trpc/tanstack-react-query). You mount its provider in _app, and prefetch a page's queries in getServerSideProps or getStaticProps when you want them in the first HTML.

We recommend a file structure like this one, although it is not enforced by tRPC. The rest of this page will take you through the process of adding tRPC in to this structure.

.
├── prisma  # <-- if prisma is added
│   └── [..]
├── src
│   ├── pages
│   │   ├── _app.tsx  # <-- mount the providers here
│   │   ├── api
│   │   │   └── trpc
│   │   │       └── [trpc].ts  # <-- tRPC HTTP handler
│   │   └── [..]
│   ├── server
│   │   ├── routers
│   │   │   ├── _app.ts  # <-- main app router
│   │   │   ├── post.ts  # <-- sub routers
│   │   │   └── [..]
│   │   ├── context.ts   # <-- create app context
│   │   └── trpc.ts      # <-- procedure helpers
│   └── utils
│       ├── query-client.ts  # <-- shared QueryClient factory
│       └── trpc.ts          # <-- your typesafe tRPC hooks
└── [..]

Add tRPC to existing Next.js project

1. Install deps

pnpm add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod
yarn add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod
bun add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod

2. Enable strict mode

If you want to use Zod for input validation, make sure you have enabled strict mode in your tsconfig.json:

tsconfig.json
"compilerOptions": {
+   "strict": true
}

If strict mode is too harsh, you'll at least want to enable strictNullChecks:

tsconfig.json
"compilerOptions": {
+   "strictNullChecks": true
}

3. Create a tRPC router

Initialize your tRPC backend in src/server/trpc.ts using the initTRPC function, and create your first router. We're going to make a simple "hello world" router and procedure here - but for deeper information on creating your tRPC API you should refer to:

View sample backend
server/trpc.ts
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';

// 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 
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
create
();
// Base router and procedure helpers export const
const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
=
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
;
export const const procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure =
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure;

server/routers/_app.ts
import { import zz } from 'zod';
import { const procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure, 
const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
} from '../trpc';
export const
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
=
router<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
hello: QueryProcedure<{
    input: {
        text: string;
    };
    output: {
        greeting: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
hello
: const procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure
.
ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
    text: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    text: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    text: string;
}, {
    text: string;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input
(
import zz.
function object<{
    text: z.ZodString;
}>(shape?: {
    text: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    text: z.ZodString;
}, z.core.$strip>
object
({
text: z.ZodStringtext: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), }), ) .
ProcedureBuilder<object, object, object, { text: string; }, { text: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
    greeting: string;
}>(resolver: ProcedureResolver<object, object, object, {
    text: string;
}, UnsetMarker, {
    greeting: string;
}>): QueryProcedure<{
    input: {
        text: string;
    };
    output: {
        greeting: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
query
((
opts: ProcedureResolverOptions<object, object, object, {
    text: string;
}>
opts
) => {
return { greeting: stringgreeting: `hello ${
opts: ProcedureResolverOptions<object, object, object, {
    text: string;
}>
opts
.
ProcedureResolverOptions<object, object, object, { text: string; }>.input: {
    text: string;
}
input
.text: stringtext}`,
}; }), }); // export type definition of API export type
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>> & DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>
AppRouter
= typeof
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
;

pages/api/trpc/[trpc].ts
import * as import trpcNexttrpcNext from '@trpc/server/adapters/next';
import { 
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter
} from '../../../server/routers/_app';
// export API handler // @link https://trpc.io/docs/server/adapters export default import trpcNexttrpcNext.
function createNextApiHandler<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: NodeHTTPHandlerOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, trpcNext.NextApiRequest, trpcNext.NextApiResponse>): trpcNext.NextApiHandler
createNextApiHandler
({
BaseHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, NextApiRequest>.router: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
router
:
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter
,
createContext?: NodeHTTPCreateContextFn<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, trpcNext.NextApiRequest, trpcNext.NextApiResponse> | undefined
createContext
: () => ({}),
});

4. Create a Query Client factory

Create utils/query-client.ts. Both _app and the pages that prefetch on the server use it.

utils/query-client.ts
import { class QueryClientQueryClient } from '@tanstack/react-query';

export function function makeQueryClient(): QueryClientmakeQueryClient() {
  return new new QueryClient(config?: QueryClientConfig): QueryClientQueryClient({
    QueryClientConfig.defaultOptions?: DefaultOptions<Error> | undefineddefaultOptions: {
      DefaultOptions<Error>.queries?: OmitKeyof<QueryObserverOptions<unknown, Error, unknown, unknown, readonly unknown[], never>, "suspense" | "queryKey", "strictly"> | undefinedqueries: {
        // keep server-rendered data fresh for a while, so the
        // client doesn't fetch it again as soon as the page loads
        staleTime?: StaleTimeFunction<unknown, Error, unknown, readonly unknown[]> | undefinedstaleTime: 30 * 1000,
      },
    },
  });
}

5. Create tRPC hooks

Use createTRPCContext to create a provider and a useTRPC hook from your API's type signature.

utils/trpc.ts
import { 
function createTRPCContext<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
    keyPrefix: false;
}>(): CreateTRPCContextResult<TRouter, TFeatureFlags>
createTRPCContext
} from '@trpc/tanstack-react-query';
import type {
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from '../server/routers/_app';
export const {
const TRPCProvider: TRPCProviderType<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
    keyPrefix: false;
}>
TRPCProvider
,
const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
    keyPrefix: false;
}>
useTRPC
} =
createTRPCContext<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
    keyPrefix: false;
}>(): CreateTRPCContextResult<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
    keyPrefix: false;
}>
createTRPCContext
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>();

6. Configure _app.tsx

Create the query client and the tRPC client once per app, and wrap every page in the providers. HydrationBoundary puts the queries a page prefetched on the server, which it returns as trpcState, into the cache.

pages/_app.tsx
import type { DehydratedState } from '@tanstack/react-query';
import { const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary, const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider } from '@tanstack/react-query';
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink } from '@trpc/client';
import type { 
type AppProps<P = any> = AppInitialProps<P> & {
    Component: NextComponentType<NextPageContext, any, any>;
    router: Router;
    __N_SSG?: boolean;
    __N_SSP?: boolean;
}
AppProps
} from 'next/app';
import { function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] (+1 overload)useState } from 'react'; import type {
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from '../server/routers/_app';
import { function makeQueryClient(): QueryClientmakeQueryClient } from '../utils/query-client'; import {
const TRPCProvider: TRPCProviderType<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
    keyPrefix: false;
}>
TRPCProvider
} from '../utils/trpc';
export default function
function App({ Component, pageProps, }: AppProps<{
    trpcState?: DehydratedState;
}>): JSX.Element
App
({
type Component: NextComponentType<NextPageContext, any, any>Component,
pageProps: {
    trpcState?: DehydratedState;
}
pageProps
,
}:
type AppProps<P = any> = AppInitialProps<P> & {
    Component: NextComponentType<NextPageContext, any, any>;
    router: Router;
    __N_SSG?: boolean;
    __N_SSP?: boolean;
}
AppProps
<{ trpcState?: DehydratedState | undefinedtrpcState?: DehydratedState }>) {
const [const queryClient: QueryClientqueryClient] = useState<QueryClient>(initialState: QueryClient | (() => QueryClient)): [QueryClient, Dispatch<SetStateAction<QueryClient>>] (+1 overload)useState(function makeQueryClient(): QueryClientmakeQueryClient); const [
const trpcClient: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpcClient
] =
useState<TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>>(initialState: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>> | (() => TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>)): [...] (+1 overload)
useState
(() =>
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
httpBatchLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink
({
// transformer: superjson, <-- if you use a data transformer url: string | URLurl: '/api/trpc', }), ], }), ); return ( <const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider client: QueryClientclient={const queryClient: QueryClientqueryClient}> <
const TRPCProvider: TRPCProviderType<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
    keyPrefix: false;
}>
TRPCProvider
trpcClient: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpcClient
={
const trpcClient: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpcClient
} queryClient: QueryClientqueryClient={const queryClient: QueryClientqueryClient}>
<const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary HydrationBoundaryProps.state: DehydratedState | null | undefinedstate={
pageProps: {
    trpcState?: DehydratedState;
}
pageProps
.trpcState?: DehydratedState | undefinedtrpcState}>
<type Component: NextComponentType<NextPageContext, any, any>Component {...
pageProps: {
    trpcState?: DehydratedState;
}
pageProps
} />
</const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary> </
const TRPCProvider: TRPCProviderType<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
    keyPrefix: false;
}>
TRPCProvider
>
</const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider> ); }

The client only calls the API from the browser, so a relative URL is enough. On the server, pages call your procedures directly, as Server-Side Rendering shows.

7. Make an API request

You're all set! Get the options for a query from useTRPC(), and pass them to TanStack React Query's hooks. For more, see TanStack React Query usage.

pages/index.tsx
import { function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<TData, TError> (+2 overloads)useQuery } from '@tanstack/react-query';
import { 
const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>, {
    keyPrefix: false;
}>
useTRPC
} from '../utils/trpc';
export default function function IndexPage(): JSX.ElementIndexPage() { const
const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>, {
    keyPrefix: false;
}>
trpc
=
function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>, {
    keyPrefix: false;
}>
useTRPC
();
const
const helloQuery: UseQueryResult<{
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>
helloQuery
=
useQuery<{
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    greeting: string;
}, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<{
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    greeting: string;
}, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<...> (+2 overloads)
useQuery
(
const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            text: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>, {
    keyPrefix: false;
}>
trpc
.
hello: DecorateQueryProcedure<{
    input: {
        text: string;
    };
    output: {
        greeting: string;
    };
    transformer: false;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> & Record<string, never>
hello
.
DecorateQueryProcedure<{ input: { text: string; }; output: { greeting: string; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<{
    greeting: string;
}, {
    greeting: string;
}>(input: {
    text: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<{
    greeting: string;
}, {
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<{
    greeting: string;
}, {
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> (+2 overloads)
queryOptions
({ text: stringtext: 'client' }));
switch (
const helloQuery: UseQueryResult<{
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>
helloQuery
.status: "pending" | "error" | "success"status) {
case 'pending': return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading...</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>; case 'error': return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Error: {
const helloQuery: QueryObserverRefetchErrorResult<{
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>> | QueryObserverLoadingErrorResult<{
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>
helloQuery
.
error: TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>
error
.TRPCClientErrorBase<DefaultErrorShape>.message: stringmessage}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
case 'success': return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{
const helloQuery: QueryObserverSuccessResult<{
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>> | QueryObserverPlaceholderResult<{
    greeting: string;
}, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>
helloQuery
.
data: {
    greeting: string;
}
data
.greeting: stringgreeting}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
} }

This page fetches in the browser. To put the data in the first HTML, prefetch it in getServerSideProps or getStaticProps.

Using a data transformer

Next.js sends page props as JSON, and checks that they are. So without a transformer, the queries you prefetch can only hold JSON values, in their inputs as well as their results: a Date or an undefined field makes getServerSideProps fail.

If your router uses a data transformer such as superjson, run the whole prefetched cache through it. In getServerSideProps or getStaticProps, return trpcState: superjson.serialize(dehydrate(queryClient)), as the SSG example does. Then read it back with a hook:

utils/use-trpc-state.ts
import type { DehydratedState } from '@tanstack/react-query';
import { function useMemo<T>(factory: () => T, deps: React.DependencyList): TuseMemo } from 'react';
import class superjsonsuperjson from 'superjson';

export type type TRPCState = SuperJSONResultTRPCState = type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyReturnType<typeof class superjsonsuperjson.SuperJSON.serialize: (object: SuperJSONValue) => SuperJSONResultserialize>;

export function function useTRPCState(trpcState: TRPCState | undefined): DehydratedState | undefineduseTRPCState(trpcState: SuperJSONResult | undefinedtrpcState: type TRPCState = SuperJSONResultTRPCState | undefined) {
  return useMemo<DehydratedState | undefined>(factory: () => DehydratedState | undefined, deps: React.DependencyList): DehydratedState | undefineduseMemo(
    () => trpcState: SuperJSONResult | undefinedtrpcState && class superjsonsuperjson.
SuperJSON.deserialize: <DehydratedState>(payload: SuperJSONResult, options?: {
    inPlace?: boolean;
}) => DehydratedState
deserialize
<DehydratedState>(trpcState: SuperJSONResulttrpcState),
[trpcState: SuperJSONResult | undefinedtrpcState], ); }

In the _app from step 6, change three things:

  • Type the props as AppProps<{ trpcState?: TRPCState }>.
  • Pass transformer: superjson to httpBatchLink.
  • Call const trpcState = useTRPCState(pageProps.trpcState), and pass trpcState to HydrationBoundary instead of pageProps.trpcState.

Moving from createTRPCNext

createTRPCNext combined the classic React Query hooks with a higher-order component for _app. Here's what replaces each part:

@trpc/next@trpc/tanstack-react-query
createTRPCNext<AppRouter>({ config })createTRPCContext<AppRouter>(), plus a createTRPCClient with your links in _app
trpc.withTRPC(MyApp)QueryClientProvider, TRPCProvider and HydrationBoundary in _app, as in step 6
trpc.post.byId.useQuery(input)useQuery(trpc.post.byId.queryOptions(input)), with trpc from useTRPC()
trpc.useUtils()useQueryClient() with trpc.post.byId.queryKey() or queryFilter()
ssr: true and ssrPrepassNo direct replacement. Prefetch each page's queries in getServerSideProps
responseMetaSet the status and headers in getServerSideProps
config({ ctx }) forwarding the request headersPass a context built from context.req to createTRPCOptionsProxy in getServerSideProps
createServerSideHelpers (@trpc/react-query/server)createTRPCOptionsProxy with a query client, then dehydrate(queryClient). See Server-Side Helpers
abortOnUnmount in configtrpc: { abortOnUnmount: true } on each query. See Aborting Procedure Calls
queryClientConfigThe options you pass to new QueryClient() in makeQueryClient

The migration guide covers the hooks in detail, and has a codemod for them that's still a work in progress.

ssr: true rendered the page over and over until no query was fetching. With getServerSideProps, you pick which queries run on the server, and Next.js keeps Automatic Static Optimization for pages that don't need them.

Common mistakes

  • Sharing one QueryClient across requests. Make it in _app with useState, and make a new one in each getServerSideProps or getStaticProps. A query client at module scope would hand one user's data to the next.
  • Returning the dehydrated state under another key. _app reads pageProps.trpcState, so return { props: { trpcState: dehydrate(queryClient) } }. Otherwise the client fetches every query again.
  • Prefetching with a different input than the page uses. The input is part of the query key, so the page misses the prefetched data and fetches again.
  • Prefetching values JSON can't hold. Next.js sends page props as JSON, so a Date or an undefined field in a query's input or result makes the page fail. Run the prefetched cache through a data transformer.
  • Mixing in App Router setup. HydrateClient and server-only are for the App Router. In the Pages Router, prefetch in getServerSideProps or getStaticProps instead.

Next steps

Browse the rest of the docs to learn more about things like authorization, middlewares, and error handling.

You can also find information about queries, mutations and invalidation in the TanStack React Query docs.

Pages under Pages Router