@trpc/next
Version:
479 lines (385 loc) • 18.4 kB
Markdown
---
name: nextjs-app-router
description: Full end-to-end tRPC setup for Next.js App Router with @trpc/tanstack-react-query. Covers the route handler with fetchRequestHandler (GET + POST exports), a per-request QueryClient that dehydrates pending queries, TRPCProvider and useTRPC() in client components, createTRPCOptionsProxy with prefetch and HydrateClient/HydrationBoundary in server components, useSuspenseQuery for Suspense, createCaller or fetchQuery for data on the server, and tRPC-powered Server Actions.
requires:
- server-setup
- client-setup
- react-query-setup
- adapter-fetch
sources:
- www/v11/docs/client/nextjs/app-router-setup.md
- www/v11/docs/client/nextjs/server-actions.md
metadata:
type: framework
library: trpc
library_version: 11.23.0
---
<!-- Generated from www/v11/docs/client/nextjs/app-router-setup.md by www/build/skills.ts. Edit the docs instead. -->
# Set up with Next.js App Router
From https://trpc.io/docs/client/nextjs/app-router-setup
> [!NOTE]
> We recommend reading TanStack React Query's [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr) 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
│ └── [..]
└── [..]
```
## Add tRPC to an existing Next.js App Router project
### 1. Install deps
```bash
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only 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](https://trpc.io/docs/quickstart) and [Backend usage docs](https://trpc.io/docs/server/overview).
```ts no-check title="trpc/init.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;
```
<br />
```ts no-check title="trpc/routers/_app.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. Create the API route handler
With the App Router, use the [fetch adapter](https://trpc.io/docs/server/adapters/fetch) to handle tRPC requests. Create a route handler that exports both `GET` and `POST`:
```ts no-check title="app/api/trpc/[trpc]/route.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 };
```
> [!NOTE]
> App Router uses the [fetch adapter](https://trpc.io/docs/server/adapters/fetch) (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.
```ts no-check title="trpc/query-client.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,
},
},
});
}
```
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 the `defaultShouldDehydrateQuery` function 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.
- `serializeData` and `deserializeData` (optional): If you set up a [data transformer](https://trpc.io/docs/server/data-transformers) 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.
```tsx no-check title="trpc/client.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>
);
}
```
Mount the provider in the root layout of your application:
```tsx no-check title="app/layout.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. 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. We'll also add a `HydrateClient` component that hands the prefetched queries to client components, a `prefetch` helper, and a `caller` for [getting data in a Server Component](https://trpc.io/docs/client/nextjs/app-router-setup#getting-data-in-a-server-component).
```tsx no-check title="trpc/server.tsx"
import 'server-only'; // <-- ensure this file cannot be imported from the client
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type { TRPCQueryOptions } 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);
const createContext = async () =>
createTRPCContext({ headers: await headers() });
export const trpc = createTRPCOptionsProxy({
ctx: createContext,
router: appRouter,
queryClient: getQueryClient,
});
// If your router is on a separate server, pass a client instead:
// createTRPCOptionsProxy({
// client: createTRPCClient({ links: [httpLink({ url: '...' })] }),
// queryClient: getQueryClient,
// });
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);
}
}
export const caller = appRouter.createCaller(createContext);
```
### 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
`prefetch` starts the query without awaiting it, and `HydrateClient` sends it to the client still pending, so Next.js streams the result in when it resolves. Until then the client's `data` is `undefined`; `await queryClient.prefetchQuery(...)` instead if you'd rather delay the page until the data is there.
```tsx no-check title="app/page.tsx"
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
```
#### Using data in a Client Component
```tsx no-check title="app/client-greeting.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>;
}
```
## Leveraging Suspense
You may prefer handling loading and error states using Suspense and Error Boundaries. You can do this by using the `useSuspenseQuery` hook.
```tsx no-check title="app/page.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>
);
}
```
```tsx no-check title="app/client-greeting.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>;
}
```
## Getting data in a Server Component
If you need access to the data in a server component, we recommend using the server `caller` from [`trpc/server.tsx`](https://trpc.io/docs/client/nextjs/app-router-setup#6-create-a-trpc-caller-for-server-components) 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](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation)
guide.
```tsx no-check title="app/page.tsx"
import { caller } from '~/trpc/server';
export default async function Home() {
const greeting = await caller.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](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation)
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:
```tsx no-check title="app/page.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>
);
}
```
## Common mistakes
- **Exporting only `POST` from the route handler.** Queries are sent as `GET` (unless the link sets `methodOverride: 'POST'`) and mutations as `POST`, so a missing export answers `405 Method Not Allowed`.
- **Importing `trpc/server.tsx` into a client component.** `server-only` makes that a build error. Client components (`'use client'`) use `useTRPC()` from `trpc/client.tsx`.
- **Prefetching with a different input than the client uses.** The input is part of the query key, so the client misses the prefetched data and fetches again.
- **Prefetching inside `HydrateClient`.** It dehydrates the cache when it renders, so it only sends queries prefetched before that. A nested server component that prefetches needs its own `HydrateClient`.
- **Sharing one `QueryClient` across requests.** On the server, only use the per-request `getQueryClient()` from `cache()`.
- **Confusing the two `createTRPCContext`s.** The one in `trpc/init.ts` creates the context your procedures get; the one from `@trpc/tanstack-react-query` creates the React provider and hooks.
## Next steps
- Learn about [Server Actions](https://trpc.io/docs/client/nextjs/server-actions) for defining tRPC-powered server actions
- Learn about [queries, mutations and invalidation](https://trpc.io/docs/client/tanstack-react-query/usage) in client components
- Explore [server-side calls](https://trpc.io/docs/server/server-side-calls) for more advanced server-side patterns
## References
- [Server Actions](references/client-nextjs-server-actions.md): Define Next.js Server Actions as tRPC procedures with experimental_caller, so they get input validation, middlewares and metadata
## 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`
- `react-query-setup` (https://trpc.io/docs/client/tanstack-react-query/setup): `npx @tanstack/intent@latest load @trpc/tanstack-react-query#react-query-setup`
- `adapter-fetch` (https://trpc.io/docs/server/adapters/fetch): `npx @tanstack/intent@latest load @trpc/server#adapter-fetch`