tRPC in Nextjs App Router

nidhinkumarnidhinkumar
7 min read

Hey, there today we will see how to setup tRPC in Nextjs App Router. Before that first lets have a quick understanding about tRPC and why tRPC is needed and then we will see how to do the basic setup of tRPC in Nextjs App Router

1.What is RPC

RPC (Remote Procedure Call) is a protocol that allows a program to execute functions on a different system (like a remote server) as if it were calling a local function. It abstracts the complexity of network communication, making distributed computing more seamless.

2.How RPC Works

A client invokes a remote procedure, serializes the parameters and additional information into a message, and sends the message to a server. On receiving the message, the server deserializes its content, executes the requested operation, and sends a result back to the client. The server stub and client stub take care of the serialization and deserialization of the parameters.

3.What is tRPC

tRPC (TypeScript Remote Procedure Call) is a framework that enables type-safe communication between a client and a server without requiring a REST API or GraphQL. It allows developers to call backend functions directly from the frontend, with full TypeScript support ensuring that types remain consistent across the application.

4.Why tRPC Needed?

  1. End-to-End Type Safety
  • With tRPC, types are shared between the client and server, reducing the risk of runtime errors caused by mismatched types.

  • Unlike REST or GraphQL, you don’t need to manually define and maintain API schemas (like OpenAPI or GraphQL SDL).

  1. No Boilerplate

    • No need to write API routes, controllers, or GraphQL resolvers—just define functions on the backend and call them from the frontend.

    • Reduces redundant code, making development faster.

  2. Better Developer Experience (DX)

    • Autocomplete and inline documentation in TypeScript provide a smooth development experience.

    • TypeScript ensures that if you change a backend function, all affected frontend calls get updated automatically.

  3. Lightweight & Fast

    • Unlike GraphQL, which requires a query parser and execution engine, tRPC calls are just direct function calls over HTTP, making it lightweight and fast.
  4. Flexible and Scalable

    • Works well with frameworks like Next.js, Express, and Fastify.

    • Can be extended with middleware for authentication, logging, and rate-limiting.

5.Install tRPC in your Nextjs project

Once you have created your Nextjs project you can install tRPC dependencies

npm install @trpc/server@next @trpc/client@next @trpc/react-query@next @tanstack/react-query@latest zod client-only server-only

Once the tRPC dependencies are installed create a new folder named “trpc” inside your src directory and inside the directory create a file named init.ts

import { initTRPC } from '@trpc/server';
import { cache } from 'react';
export const createTRPCContext = cache(async () => {
  /**
   * @see: https://trpc.io/docs/server/context
   */
  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.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;

Inside the trpc directory create another folder named routers and inside the routers create another file named _app.ts and paste the below code

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;

Now inside the app folder create a new folder named api and inside the api folder create another folder named trpc

The folder structure should look like app/api/trpc/[trpc]/route.ts. In the route.ts file add the below snippet

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,
  });
export { handler as GET, handler as POST };

Now we will create the client query factory. Create a shared file trpc/query-client.ts that exports a function that creates a QueryClient instance

Inside the query-client.ts file add the below snippets

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

Now we will create a tRPC client for Client Components. The trpc/client.tsx is the entrypoint when consuming your tRPC API from client components

'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 { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
export const trpc = createTRPCReact<AppRouter>();
let clientQueryClientSingleton: QueryClient;
function getQueryClient() {
  if (typeof window === 'undefined') {
    // Server: always make a new query client
    return makeQueryClient();
  }
  // Browser: use singleton pattern to keep the same query client
  return (clientQueryClientSingleton ??= makeQueryClient());
}
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 TRPCProvider(
  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(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          // transformer: superjson, <-- if you use a data transformer
          url: getUrl(),
        }),
      ],
    }),
  );
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {props.children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

To prefetch queries from server components, we use a tRPC caller. The @trpc/react-query/rsc module exports a thin wrapper around createCaller that integrates with your React Query client.

import 'server-only'; // <-- ensure this file cannot be imported from the client
import { createHydrationHelpers } from '@trpc/react-query/rsc';
import { cache } from 'react';
import { createCallerFactory, 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 caller = createCallerFactory(appRouter)(createTRPCContext);
export const { trpc, HydrateClient } = createHydrationHelpers<typeof appRouter>(
  caller,
  getQueryClient,
);

Now we will use tRPC API in our app. While you can use the React Query hooks in client components just like you would in any other React app, we can take advantage of the RSC capabilities by prefetching queries in a server component high up the tree.

import { trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
  void trpc.hello.prefetch();
  return (
    <HydrateClient>
      <div>...</div>
      {/** ... */}
      <ClientGreeting />
    </HydrateClient>
  );
}

In the ClientGreeting component

'use client';
// <-- hooks can only be used in client components
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
  const greeting = trpc.hello.useQuery();
  if (!greeting.data) return <div>Loading...</div>;
  return <div>{greeting.data.greeting}</div>;
}

If you would like to use the loading and the error states using suspense and Error boundaries. You can do this by using the useSuspenseQuery hook.

import { trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
  void trpc.hello.prefetch();
  return (
    <HydrateClient>
      <div>...</div>
      {/** ... */}
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <Suspense fallback={<div>Loading...</div>}>
          <ClientGreeting />
        </Suspense>
      </ErrorBoundary>
    </HydrateClient>
  );
}

In the ClientGreeting component you can get the data using the useSuspenseQuery

'use client';
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
  const [data] = trpc.hello.useSuspenseQuery();
  return <div>{data.greeting}</div>;
}

Voila!!! You have completed setting up tRPC in your Next.js project.

Will catchup in a new post till then Happy Learning :)

0
Subscribe to my newsletter

Read articles from nidhinkumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

nidhinkumar
nidhinkumar