Simple, up-to-date, copy-paste setup guide for your Next.js App Router + tRPC setup.
Usage:
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.
Top comments (0)