nextjs-pages-router
Set up tRPC in Next.js Pages Router with createNextApiHandler and @trpc/tanstack-react-query. Covers a QueryClient factory, TRPCProvider, QueryClientProvider and HydrationBoundary in _app, useTRPC() with useQuery in pages, and prefetching a page's queries in getServerSideProps or getStaticProps with createTRPCOptionsProxy and dehydrate, returned as trpcState. Replaces the deprecated createTRPCNext, withTRPC, ssr true, ssrPrepass and createServerSideHelpers.
Set up with Next.js Pages Router
From https://trpc.io/docs/client/nextjs/pages-router/setup
Caution
This guide is for the Next.js Pages Router. If you are using the Next.js App Router, see the App Router setup guide instead.
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.
Deprecated
createTRPCNext, withTRPC and ssr: true from @trpc/next are deprecated. They're built on the classic React Query integration, which only gets bug fixes and will be removed in the next major version of tRPC. To move an existing app, see Moving from createTRPCNext.
Recommended file structure
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
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod2. Enable strict mode
If you want to use Zod for input validation, make sure you have enabled strict mode in your tsconfig.json:
"compilerOptions": {
+ "strict": true
}If strict mode is too harsh, you'll at least want to enable strictNullChecks:
"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:
- the Quickstart guide and Backend usage docs for tRPC information
- the Next.js Adapter docs for mounting tRPC within your Next.js server.
View sample backend
import { 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 t = initTRPC.create();
// Base router and procedure helpers
export const router = t.router;
export const procedure = t.procedure;import { z } from 'zod';
import { procedure, router } from '../trpc';
export const appRouter = router({
hello: procedure
.input(
z.object({
text: z.string(),
}),
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;import * as trpcNext from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';
// export API handler
// @link https://trpc.io/docs/server/adapters
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: () => ({}),
});Note
The backend above is using the recommended file structure, but you can keep it simple and put everything in an API handler directly if you prefer.
4. Create a Query Client factory
Create utils/query-client.ts. Both _app and the pages that prefetch on the server use it.
import { QueryClient } from '@tanstack/react-query';
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// keep server-rendered data fresh for a while, so the
// client doesn't fetch it again as soon as the page loads
staleTime: 30 * 1000,
},
},
});
}5. Create tRPC hooks
Use createTRPCContext to create a provider and a useTRPC hook from your API's type signature.
import { createTRPCContext } from '@trpc/tanstack-react-query';
import type { AppRouter } from '../server/routers/_app';
export const { TRPCProvider, useTRPC } = createTRPCContext<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.
import type { DehydratedState } from '@tanstack/react-query';
import { HydrationBoundary, QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppProps } from 'next/app';
import { useState } from 'react';
import type { AppRouter } from '../server/routers/_app';
import { makeQueryClient } from '../utils/query-client';
import { TRPCProvider } from '../utils/trpc';
export default function App({
Component,
pageProps,
}: AppProps<{ trpcState?: DehydratedState }>) {
const [queryClient] = useState(makeQueryClient);
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: '/api/trpc',
}),
],
}),
);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
<HydrationBoundary state={pageProps.trpcState}>
<Component {...pageProps} />
</HydrationBoundary>
</TRPCProvider>
</QueryClientProvider>
);
}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.
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '../utils/trpc';
export default function IndexPage() {
const trpc = useTRPC();
const helloQuery = useQuery(trpc.hello.queryOptions({ text: 'client' }));
switch (helloQuery.status) {
case 'pending':
return <p>Loading...</p>;
case 'error':
return <p>Error: {helloQuery.error.message}</p>;
case 'success':
return <p>{helloQuery.data.greeting}</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:
import type { DehydratedState } from '@tanstack/react-query';
import { useMemo } from 'react';
import superjson from 'superjson';
export type TRPCState = ReturnType<typeof superjson.serialize>;
export function useTRPCState(trpcState: TRPCState | undefined) {
return useMemo(
() => trpcState && superjson.deserialize<DehydratedState>(trpcState),
[trpcState],
);
}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 ssrPrepass | No direct replacement. Prefetch each page's queries in getServerSideProps |
| responseMeta | Set the status and headers in getServerSideProps |
| config({ ctx }) forwarding the request headers | Pass 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 config | trpc: { abortOnUnmount: true } on each query. See Aborting Procedure Calls |
| queryClientConfig | The 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.
Server-Side Helpers
From https://trpc.io/docs/client/nextjs/pages-router/server-side-helpers
To run queries on the server in getServerSideProps or getStaticProps, make a query client and pass it to createTRPCOptionsProxy from @trpc/tanstack-react-query. The proxy gives you the same queryOptions as useTRPC() in a component. You prefetch them into the query client, then send its cache to the page, so the page doesn't have to fetch them again.
Deprecated
createServerSideHelpers from @trpc/react-query/server belongs to the classic React Query integration, which only gets bug fixes and will be removed in the next major version of tRPC. Use createTRPCOptionsProxy instead, as below.
Create the proxy
There are two ways to create it, depending on where your router runs.
1. Internal router
Use this when your router is in the same Next.js app. The proxy calls your procedures directly, with no HTTP request, as server-side calls do. So pass it a context: build it from context.req in getServerSideProps, as SSR shows. In getStaticProps there's no request, so the context can't need one. The "inner" and "outer" context pattern helps here.
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { createContext } from './server/context';
import { appRouter } from './server/routers/_app';
import { makeQueryClient } from './utils/query-client';
export async function getStaticProps() {
const queryClient = makeQueryClient();
const trpc = createTRPCOptionsProxy({
router: appRouter,
ctx: await createContext(),
queryClient,
});
// ...
}2. External router
Use this when your API runs on another server. Pass a tRPC client instead of the router:
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type { AppRouter } from './server/router';
import { makeQueryClient } from './utils/query-client';
export async function getStaticProps() {
const queryClient = makeQueryClient();
const trpc = createTRPCOptionsProxy({
client: createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
}),
queryClient,
});
// ...
}Make the query client and the proxy inside getServerSideProps or getStaticProps, so each call gets new ones. A query client at module scope would share one user's data with the next.
Prefetch, fetch and dehydrate
Run the queries with the query client's methods:
- queryClient.prefetchQuery(trpc.post.byId.queryOptions(input)) puts the result in the cache. It never throws and returns nothing.
- queryClient.fetchQuery(trpc.post.byId.queryOptions(input)) also caches the result, but returns it, and throws if the query fails.
- prefetchInfiniteQuery and fetchInfiniteQuery do the same with trpc.post.list.infiniteQueryOptions(input).
Use prefetchQuery for data the page shows, and fetchQuery when you need the result on the server, for instance to return notFound. Then send the cache to the page with dehydrate:
// In getServerSideProps / getStaticProps:
const props = {
// `_app` reads this key
trpcState: dehydrate(queryClient),
};These are TanStack React Query's own functions. See their docs for more.
Coming from createServerSideHelpers, helpers.post.byId.prefetch(input) is now queryClient.prefetchQuery(trpc.post.byId.queryOptions(input)), fetch is fetchQuery, and helpers.dehydrate() is dehydrate(queryClient). If you passed a transformer to the helpers, run the dehydrated state through it yourself, as Using a data transformer shows.
Next.js example
import { dehydrate, useQuery } from '@tanstack/react-query';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from 'next';
import { appRouter } from '../../server/routers/_app';
import { makeQueryClient } from '../../utils/query-client';
import { useTRPC } from '../../utils/trpc';
export async function getServerSideProps(
context: GetServerSidePropsContext<{ id: string }>,
) {
const id = context.params?.id as string;
const queryClient = makeQueryClient();
const trpc = createTRPCOptionsProxy({
router: appRouter,
ctx: {},
queryClient,
});
await queryClient.prefetchQuery(trpc.post.byId.queryOptions({ id }));
return {
props: {
trpcState: dehydrate(queryClient),
id,
},
};
}
export default function PostViewPage(
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) {
const trpc = useTRPC();
const postQuery = useQuery(trpc.post.byId.queryOptions({ id: props.id }));
switch (postQuery.status) {
case 'pending':
return <p>Loading...</p>;
case 'error':
return <p>Error: {postQuery.error.message}</p>;
case 'success':
return (
<article>
<h1>{postQuery.data.title}</h1>
<p>{postQuery.data.text}</p>
</article>
);
}
}Choosing between App Router and Pages Router
| App Router | Pages Router | |
|---|---|---|
| Recommended for | New projects | Existing Pages Router projects |
| Data fetching | Server Components, prefetchQuery | prefetchQuery in getServerSideProps or getStaticProps |
| Server adapter | Fetch adapter | Next.js adapter |
| Client package | @trpc/tanstack-react-query | @trpc/tanstack-react-query |
| Provider setup | TRPCProvider in the root layout | TRPCProvider and HydrationBoundary in _app |
Tip
If you're starting a new project, we recommend the App Router. If you have an existing Pages Router project, tRPC works well with it and is fully supported.
From https://trpc.io/docs/client/nextjs#choosing-between-app-router-and-pages-router
References
- Server-Side Rendering: Prefetch a page's tRPC queries in getServerSideProps with createTRPCOptionsProxy, build the context from the request, and set the status and cache headers
- Static Site Generation: Prefetch tRPC queries in getStaticProps with createTRPCOptionsProxy and dehydrate, and stop the client from refetching them
- Next.js Adapter: Serve tRPC from a Next.js API route with createNextApiHandler, or from an App Router route handler with fetchRequestHandler
- Aborting Procedure Calls: Cancel in-flight requests when a component unmounts with the abortOnUnmount option on a query
- TanStack React Query: The options and key factories on the trpc proxy, such as queryOptions, infiniteQueryOptions, mutationOptions, subscriptionOptions, queryKey, queryFilter and pathFilter, plus key prefixing and type inference
- Migrating from the classic React Client: Migrating from the classic React Client
Related skills
- server-setup (https://trpc.io/docs/server/overview): npx @tanstack/intent@latest load @trpc/server#server-setup
- client-setup (https://trpc.io/docs/client/vanilla/setup): npx @tanstack/intent@latest load @trpc/client#client-setup