tRPC 11 Setup for Next.js App router 2025


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>;
}
Enter fullscreen mode

Exit fullscreen mode



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>
  );
}
Enter fullscreen mode

Exit fullscreen mode

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
Enter fullscreen mode

Exit fullscreen mode

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
Enter fullscreen mode

Exit fullscreen mode



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
Enter fullscreen mode

Exit fullscreen mode



tsconfig.json

Make sure strict mode is set to true in tsconfig

"compilerOptions": {
  "strict": true
  //...
}
Enter fullscreen mode

Exit fullscreen mode



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

Enter fullscreen mode

Exit fullscreen mode



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

Enter fullscreen mode

Exit fullscreen mode



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;
Enter fullscreen mode

Exit fullscreen mode



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

Enter fullscreen mode

Exit fullscreen mode



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>();
Enter fullscreen mode

Exit fullscreen mode



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>
  );
}
Enter fullscreen mode

Exit fullscreen mode



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 };
Enter fullscreen mode

Exit fullscreen mode



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

Enter fullscreen mode

Exit fullscreen mode

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

For more details on how to use tRPC.



References



Source link

Leave a Reply

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