tRPC supports the new App Router out of the box, but there are multiple ways you might want to utilise it for your projects depending on your need and project structure. In this article, we’ll present all the possible ways and also showcase some of the experimental things we’ve been working on.

::: For a full example, check out the next-app-dir example on GitHub.

Route Handlers

While the old pages router’s API handlers use the Express-like API with Incoming Message and Outgoing Message, the app router is based on the web standard Request and Response. This means the old way of creating your API handler doesn’t work in the app router. Instead we’ll use the more general Fetch adapter to create our API handler:

// src/app/api/trpc/[trpc]/route.ts
- import { createNextApiHandlere } from "@trpc/server/adapters/next";
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';

- export default createNextApiHandler({
-   router: appRouter,
-   createContext,
- });
+ const handler = (request: Request) =>
+   fetchRequestHandler({
+     endpoint: '/api/trpc',
+     req: request,
+     router: appRouter,
+     createContext,
+   });
+ export { handler as GET, handler as POST };

Initializing the tRPC Client

There are multiple ways you can call your router. Below we present a few different options:

In Server Components

You can use the vanilla client in server components which allows for using async/await:

// src/trpc/server.ts
import { 
  createTRPCProxyClient,
  unstable_httpBatchStreamLink
} from "@trpc/client";
import type { AppRouter } from "~/server/trpc/router";

export const trpc = createTRPCProxyClient<AppRouter>({
  // transformer,
  links: [
    unstable_httpBatchStreamLink({
      url: BASE_URL + "/api/trpc",
    }),
  ],
});

Tip: The above client uses the relatively new httpBatchStreamLink. This allows for better utilization of streaming using suspense. You can of course use the normal httpBatchLink too, but beware that your <Suspense> blocks will all resolve at the same time then. By using the streaming link, the queries are all fired in batch, but each query is resolved as soon as it finishes.

We can then call our queries in server components:

import { trpc } from "~/trpc/server";

export default async function Page() {
  const posts = await trpc.posts.list.query();

  return (
    <div>
      {posts.map((post) => <PostCard post={post} />)}
    </div>
  );
}

Note: that if <PostCard /> is a client component, post must be JSON-serialisable, so e.g. a Date object will be transformed to a string when crossing the server-client border.

In Client Components

If you wanna query data in client components, you can keep using our React Query integration similar to before. The main change is that you have to wrap your app in the necessary providers yourself, instead of calling trpc.withTRPC(<App />) like before. You’ll also need to forward the incoming requests headers by calling headers() in a server component and passing those down to the provider as props

// src/trpc/react.tsx
"use client";

import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { unstable_httpBatchStreamLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "~/server/trpc/router";

export const trpc = createTRPCReact<AppRouter>();

export function TRPCReactProvider(props: {
  children: React.ReactNode;
  headers: Headers; // <-- Important
}) {
  const [queryClient] = useState(
    () => new QueryClient(),
  );

  const [trpcClient] = useState(() =>
    trpc.createClient({
      // transformer,
      links: [
        unstable_httpBatchStreamLink({
          headers() {
            const headers = new Map(props.headers);
            return Object.fromEntries(headers);
          },
        }),
      ],
    }),
  );

  return (
    <QueryClientProvider client={queryClient}>
      <trpc.Provider client={trpcClient} queryClient={queryClient}>
        {props.children}
      </trpc.Provider>
    </QueryClientProvider>
  );
}

Then wrap your app with the created providers:

// src/app/layout.tsx
import { headers } from "next/headers";
import { TRPCReactProviders } from "~/trpc/react";

export default function RootLayout() {
  return (
    <html lang="en">
        <body>
          <TRPCReactProvider
            headers={headers()} // <-- forward headers
          >
            {props.children}
          </TRPCReactProvider>
        </body>
    </html>
  );
}

Now you’re ready to use tRPC with React Query just like you’re used to. We do however recommend using the useSuspenseQuery hook to fully utilize the new and powerful streaming with suspense patterns.