tRPC logo

Skill

nextjs-pages-router

integrate tRPC in Next.js Pages Router

Published by tRPC Updated Jul 18
Covers Next.js tRPC SSR Frontend

Description

Set up tRPC in Next.js Pages Router with createNextApiHandler, createTRPCNext, withTRPC HOC, SSR via ssr option and ssrPrepass, SSG via createServerSideHelpers with getStaticProps, and server-side helpers for getServerSideProps prefetching.

SKILL.md

This skill builds on server-setup and client-setup. Read them first for foundational concepts.

tRPC -- Next.js Pages Router

File Structure

.
├── src
│   ├── pages
│   │   ├── _app.tsx              # withTRPC() HOC
│   │   ├── api/trpc
│   │   │   └── [trpc].ts        # tRPC API handler
│   │   └── index.tsx             # page using tRPC hooks
│   ├── server
│   │   ├── routers
│   │   │   └── _app.ts          # main app router
│   │   ├── context.ts           # createContext
│   │   └── trpc.ts              # initTRPC, procedure helpers
│   └── utils
│       └── trpc.ts              # createTRPCNext, hooks
└── ...

Setup

1. Install

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

2. Server init

import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const procedure = t.procedure;

3. Define the router

import { z } from 'zod';
import { procedure, router } from '../trpc';

export const appRouter = router({
  hello: procedure.input(z.object({ text: z.string() })).query(({ input }) => ({
    greeting: `hello ${input.text}`,
  })),
});

export type AppRouter = typeof appRouter;

4. API handler

trpc
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';

export default createNextApiHandler({
  router: appRouter,
  createContext: () => ({}),
});

5. Create tRPC hooks

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers/_app';

function getBaseUrl() {
  if (typeof window !== 'undefined') return '';
  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
  return `http://localhost:${process.env.PORT ?? 3000}`;
}

export const trpc = createTRPCNext<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
        }),
      ],
    };
  },
  ssr: false,
});

6. Wrap app with withTRPC HOC

import type { AppType } from 'next/app';
import { trpc } from '../utils/trpc';

const MyApp: AppType = ({ Component, pageProps }) => {
  return <Component {...pageProps} />;
};

export default trpc.withTRPC(MyApp);

7. Use hooks in pages

import { trpc } from '../utils/trpc';

export default function IndexPage() {
  const hello = trpc.hello.useQuery({ text: 'client' });
  if (!hello.data) return <div>Loading...</div>;
  return <p>{hello.data.greeting}</p>;
}

Core Patterns

SSR with ssr: true

Enable SSR to prefetch all queries on the server automatically. Requires ssrPrepass and forwarding client headers.

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from '../server/routers/_app';

function getBaseUrl() {
  if (typeof window !== 'undefined') return '';
  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
  return `http://localhost:${process.env.PORT ?? 3000}`;
}

export const trpc = createTRPCNext<AppRouter>({
  ssr: true,
  ssrPrepass,
  config({ ctx }) {
    if (typeof window !== 'undefined') {
      return {
        links: [httpBatchLink({ url: '/api/trpc' })],
      };
    }
    return {
      links: [
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
          headers() {
            if (!ctx?.req?.headers) return {};
            return { cookie: ctx.req.headers.cookie };
          },
        }),
      ],
    };
  },
});

SSG with createServerSideHelpers and getStaticProps

id
import { createServerSideHelpers } from '@trpc/react-query/server';
import type {
  GetStaticPaths,
  GetStaticPropsContext,
  InferGetStaticPropsType,
} from 'next';
import superjson from 'superjson';
import { appRouter } from '../../server/routers/_app';
import { trpc } from '../../utils/trpc';

export async function getStaticProps(
  context: GetStaticPropsContext<{ id: string }>,
) {
  const helpers = createServerSideHelpers({
    router: appRouter,
    ctx: {},
    transformer: superjson,
  });
  const id = context.params?.id as string;

  await helpers.post.byId.prefetch({ id });

  return {
    props: {
      trpcState: helpers.dehydrate(),
      id,
    },
    revalidate: 1,
  };
}

export const getStaticPaths: GetStaticPaths = async () => {
  return { paths: [], fallback: 'blocking' };
};

export default function PostPage(
  props: InferGetStaticPropsType<typeof getStaticProps>,
) {
  const { id } = props;
  const postQuery = trpc.post.byId.useQuery({ id });

  if (postQuery.status !== 'success') return <>Loading...</>;
  return <h1>{postQuery.data.title}</h1>;
}

Server-side helpers with getServerSideProps

id
import { createServerSideHelpers } from '@trpc/react-query/server';
import type {
  GetServerSidePropsContext,
  InferGetServerSidePropsType,
} from 'next';
import superjson from 'superjson';
import { appRouter } from '../../server/routers/_app';
import { trpc } from '../../utils/trpc';

export async function getServerSideProps(
  context: GetServerSidePropsContext<{ id: string }>,
) {
  const helpers = createServerSideHelpers({
    router: appRouter,
    ctx: {},
    transformer: superjson,
  });
  const id = context.params?.id as string;

  await helpers.post.byId.prefetch({ id });

  return {
    props: {
      trpcState: helpers.dehydrate(),
      id,
    },
  };
}

export default function PostPage(
  props: InferGetServerSidePropsType<typeof getServerSideProps>,
) {
  const { id } = props;
  const postQuery = trpc.post.byId.useQuery({ id });

  if (postQuery.status !== 'success') return <>Loading...</>;
  return <h1>{postQuery.data.title}</h1>;
}

SSR response caching

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from '../server/routers/_app';

export const trpc = createTRPCNext<AppRouter>({
  ssr: true,
  ssrPrepass,
  config() {
    return {
      links: [httpBatchLink({ url: '/api/trpc' })],
    };
  },
  responseMeta(opts) {
    const { clientErrors } = opts;
    if (clientErrors.length) {
      return { status: clientErrors[0].data?.httpStatus ?? 500 };
    }
    const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
    return {
      headers: new Headers([
        [
          'cache-control',
          `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
        ],
      ]),
    };
  },
});

CORS on the API handler

trpc
import { createNextApiHandler } from '@trpc/server/adapters/next';
import type { NextApiRequest, NextApiResponse } from 'next';
import { appRouter } from '../../../server/routers/_app';

const nextApiHandler = createNextApiHandler({
  router: appRouter,
  createContext: () => ({}),
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
  res.setHeader('Access-Control-Allow-Headers', '*');

  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    return res.end();
  }

  return nextApiHandler(req, res);
}

Limiting batch size with maxBatchSize

trpc
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';

export default createNextApiHandler({
  router: appRouter,
  createContext: () => ({}),
  maxBatchSize: 10,
});

Requests batching more than maxBatchSize operations are rejected with a 400 Bad Request error. Set maxItems on your client's httpBatchLink to the same value to avoid exceeding the limit.

Common Mistakes

Using ssr: true without understanding implications

Enabling ssr: true imports react-dom and runs ssrPrepass on every request, rendering the component tree repeatedly until no queries are fetching. This adds latency and server load. For better control, keep ssr: false (the default) and use createServerSideHelpers in getServerSideProps or getStaticProps to selectively prefetch only the queries you need.

SSR prepass renders multiple times

The SSR prepass loop re-renders the component tree repeatedly until all queries resolve. This is by design but causes performance issues with expensive renders. Keep SSR-rendered pages lightweight, or switch to selective prefetching with server-side helpers.

Mixing App Router and Pages Router patterns

App Router uses fetchRequestHandler, createTRPCOptionsProxy, and @trpc/tanstack-react-query. Pages Router uses createNextApiHandler, createTRPCNext, and @trpc/next/@trpc/react-query. Applying App Router patterns (like HydrationBoundary or prefetchQuery) in Pages Router, or vice versa, produces non-functional code.

Forgetting to return trpcState from getStaticProps/getServerSideProps

When using createServerSideHelpers, you must return trpcState: helpers.dehydrate() in props. Without this, the prefetched data is lost and queries re-fetch on the client.

// WRONG
return { props: { id } }; // missing trpcState!

// CORRECT
return { props: { trpcState: helpers.dehydrate(), id } };

See Also

  • server-setup -- initTRPC, routers, procedures, context
  • client-setup -- vanilla tRPC client, links configuration
  • nextjs-app-router -- if migrating to or starting with App Router
  • react-query-classic-migration -- migrating from @trpc/react-query to @trpc/tanstack-react-query

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.