Simple, up-to-date, copy-paste setup guide for your Next.js App Router + tRPC setup.

Client

Using the tRPC v11 client with the new syntax.

"use client"; import { useTRPC } from "@/trpc/utils"; import { useQuery } from "@tanstack/react-query"; export function HelloClient() { const trpc = useTRPC(); // ↓↓↓↓↓ New Syntax! ↓↓↓↓↓ const { data, status } = useQuery(trpc.hello.queryOptions()); return <div>{status}: {data}</div>; } 

Prefetch on server-side

Prefetch on the server side to get faster loads.

import { trpc, prefetch, HydrateClient } from "@/trpc/server"; import { HelloClient } from "./client"; export default async function Page() { void prefetch(trpc.hello.queryOptions()); return ( <HydrateClient> <HelloClient /> </HydrateClient> ); } 

Use await instead of void if you want to completely avoid streaming and client-side loading.

Setup

Folder structure:

You may use a src folder if needed.

/app ├── api │ └── trpc │ └── [trpc] │ └── route.ts ├── layout.tsx ├── page.tsx /trpc ├── client.tsx ├── init.ts ├── query-client.tsx ├── router.ts ├── server.tsx └── utils.ts 

I like to consolidate everything into one tRPC folder for both front and backend. This makes tRPC config easy to manage.

Please setup Next.js if you haven’t already.

pnpm create next-app@latest my-app --yes 

1. Install deps

Use pnpm or use your package manager of choice

pnpm add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only superjson 

tsconfig.json

Make sure strict mode is set to true in tsconfig

"compilerOptions": { "strict": true //... } 

1. /trpc/query-client.ts

The TanStack Query Client setup. Superjson is optional but highly recommended for being able to send instances such as the Date object between front & backend.

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, }, }, }); } 

2. /trpc/init.ts

This is where you define your base tRPC procedures, middleware, and context. Use your auth of choice.

import { validateRequest } from "@/app/auth"; import { cache } from "react"; import superjson from "superjson"; import { TRPCError, initTRPC } from "@trpc/server"; export const createTRPCContext = cache(async () => { /** * @see: https://trpc.io/docs/server/context */ // Your custom auth logic here const { session, user } = await validateRequest(); return { session, user, }; }); type Context = Awaited<ReturnType<typeof createTRPCContext>>; /** * Initialization of tRPC backend * Should be done only once per backend! */ const t = initTRPC.context<Context>().create({ transformer: superjson, }); /** * Export reusable router and procedure helpers * that can be used throughout the router */ export const router = t.router; export const publicProcedure = t.procedure; export const createCallerFactory = t.createCallerFactory; export const authenticatedProcedure = t.procedure.use(async (opts) => { if (!opts.ctx.user || !opts.ctx.session) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Unauthorized", }); } return opts.next({ ctx: { user: opts.ctx.user, session: opts.ctx.user, }, }); }); 

3. /trpc/router.ts

This is where you customize your trpc routers for your use-case.

import { publicProcedure, router } from "./init"; export const appRouter = router({ hello: publicProcedure.query(async () => { await new Promise((resolve) => setTimeout(resolve, 500)); return "Hello World"; }), }); // Export type router type signature, // NOT the router itself. export type AppRouter = typeof appRouter; 

4. /trpc/server.tsx

This is new with tRPC v11 that enables prefetching on the server-side.

import "server-only"; // <-- ensure this file cannot be imported from the client import { createTRPCOptionsProxy, TRPCQueryOptions, } from "@trpc/tanstack-react-query"; import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; import { cache } from "react"; import { createTRPCContext } from "./init"; import { makeQueryClient } from "./query-client"; import { appRouter } from "./router"; // 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: createTRPCContext, router: appRouter, queryClient: getQueryClient, }); // Optional: Prefetch helper function export function HydrateClient(props: { children: React.ReactNode }) { const queryClient = getQueryClient(); return ( <HydrationBoundary state={dehydrate(queryClient)}> {props.children} </HydrationBoundary> ); } // Optional: Prefetch helper function // eslint-disable-next-line @typescript-eslint/no-explicit-any export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>( queryOptions: T ) { const queryClient = getQueryClient(); if (queryOptions.queryKey[1]?.type === "infinite") { // eslint-disable-next-line @typescript-eslint/no-explicit-any void queryClient.prefetchInfiniteQuery(queryOptions as any); } else { void queryClient.prefetchQuery(queryOptions); } } 

5. /trpc/utils.ts

Exports the React provider and hooks with the correct types.

import { createTRPCContext } from "@trpc/tanstack-react-query"; import type { AppRouter } from "./router"; export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext<AppRouter>(); 

6. /trpc/client.tsx

Create the React provider that will be used by your app, integrating TanStack Query & tRPC.

"use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createTRPCClient, httpBatchLink } from "@trpc/client"; import { useState } from "react"; import { TRPCProvider } from "./utils"; import { AppRouter } from "./router"; import superjson from "superjson"; import { makeQueryClient } from "./query-client"; function getBaseUrl() { if (typeof window !== "undefined") // browser should use relative path return ""; if (process.env.VERCEL_URL) // reference for vercel.com return `https://${process.env.VERCEL_URL}`; if (process.env.RENDER_INTERNAL_HOSTNAME) // reference for render.com return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; // assume localhost return `http://localhost:${process.env.PORT ?? 3000}`; } let browserQueryClient: QueryClient | undefined = undefined; function getQueryClient() { if (typeof window === "undefined") { // Server: always make a new query client return makeQueryClient(); } else { // 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; } } export function QueryProvider({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient(); const [trpcClient] = useState(() => createTRPCClient<AppRouter>({ links: [ httpBatchLink({ url: `${getBaseUrl()}/api/trpc`, transformer: superjson, }), ], }) ); return ( <QueryClientProvider client={queryClient}> <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}> {children} </TRPCProvider>  </QueryClientProvider>  ); } 

7. Mount your tRPC api endpoint at /app/api/trpc/[trpc]/route.ts

import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { createTRPCContext } from "@/trpc/init"; import { appRouter } from "@/trpc/router"; const handler = (req: Request) => fetchRequestHandler({ endpoint: "/api/trpc", req, router: appRouter, createContext: createTRPCContext, }); export { handler as GET, handler as POST }; 

8. App router root layout /app/layout.tsx

Finally, wrap your app with the QueryProvider.

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

And there you have it! Start using tRPC in your Next.js app router.

For more details on how to use tRPC.

References


Source: DEV Community.


Leave a Reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.