dev_tools
Back to guides

tRPC v11 + Next.js App Router

Build fully type-safe APIs that share types between your Next.js server and client — no schemas, no code generation. Uses the new @trpc/tanstack-react-query integration.

tRPC
Next.js
TypeScript
TanStack Query
1

Create Next.js Project

Scaffold a new Next.js app with TypeScript and the App Router.

npx create-next-app@latest my-trpc-app --typescript --tailwind --app --src-dir
2

Install tRPC v11 and Dependencies

tRPC v11 uses @trpc/tanstack-react-query — the new TanStack Query-native client that replaces the legacy @trpc/react-query. Also install server-only and client-only to enforce module boundaries. See migration guide.

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

Initialize the tRPC Instance

Create src/server/trpc.ts. initTRPC.create() returns the builder you use to define all procedures and routers. The server-only import prevents this from being accidentally imported in client components.

src/server/trpc.ts
import 'server-only'
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

export const router = t.router
export const publicProcedure = t.procedure
4

Create the App Router

Define your procedures in src/server/routers/app.ts. Queries return data, mutations change it. Zod validates all inputs at runtime — a mismatch throws a 400 before your resolver runs.

src/server/routers/app.ts
import 'server-only'
import { z } from 'zod'
import { router, publicProcedure } from '../trpc'

const usersRouter = router({
  list: publicProcedure.query(async () => {
    "text-muted-foreground italic">// Replace with a real DB call
    return [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob',   email: 'bob@example.com' },
    ]
  }),

  byId: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      return { id: input.id, name: 'Alice', email: 'alice@example.com' }
    }),

  create: publicProcedure
    .input(z.object({
      name:  z.string().min(1, 'Name is required'),
      email: z.string().email('Invalid email'),
    }))
    .mutation(async ({ input }) => {
      return { id: Date.now(), ...input }
    }),
})

export const appRouter = router({ users: usersRouter })

export type AppRouter = typeof appRouter
5

Create the Next.js API Route Handler

Mount the tRPC router on a Next.js catch-all Route Handler using the fetch adapter — required for the App Router since it uses Web-standard Request/Response.

src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/server/routers/app'
import type { NextRequest } from 'next/server'

const handler = (req: NextRequest) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  })

export { handler as GET, handler as POST }
6

Create the Typed tRPC Client

Create src/lib/trpc/client.ts. In tRPC v11, createTRPCContext produces a TRPCProvider and a useTRPC hook — all fully typed from your AppRouter. The client-only import prevents server components from accidentally calling this.

src/lib/trpc/client.ts
import 'client-only'
import { createTRPCContext } from '@trpc/tanstack-react-query'
import type { AppRouter } from '@/server/routers/app'

export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>()
7

Create the Providers Component

Wrap your app with the TanStack Query client and the tRPC provider. httpBatchStreamLink batches multiple tRPC calls into a single HTTP request and supports streaming responses.

src/components/Providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createTRPCClient, httpBatchStreamLink } from '@trpc/client'
import { useState } from 'react'
import type { AppRouter } from '@/server/routers/app'
import { TRPCProvider } from '@/lib/trpc/client'

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())
  const [trpcClient] = useState(() =>
    createTRPCClient<AppRouter>({
      links: [
        httpBatchStreamLink({ url: '/api/trpc' }),
      ],
    })
  )

  return (
    <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </TRPCProvider>
  )
}
8

Wrap Root Layout with Providers

Import your Providers component in the root layout.

src/app/layout.tsx
import { Providers } from '@/components/Providers'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
9

Use tRPC in a Client Component

In tRPC v11, useTRPC() returns the typed router. Pass the result to TanStack Query hooks via .queryOptions() and .mutationOptions(). Full autocomplete and inference — no manual types needed.

src/app/users/page.tsx
'use client'

import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTRPC } from '@/lib/trpc/client'

export default function UsersPage() {
  const trpc = useTRPC()
  const queryClient = useQueryClient()

  const { data: users, isLoading } = useQuery(trpc.users.list.queryOptions())

  const createUser = useMutation(
    trpc.users.create.mutationOptions({
      onSuccess: () => {
        queryClient.invalidateQueries(trpc.users.list.queryFilter())
      },
    })
  )

  const [name, setName]   = useState('')
  const [email, setEmail] = useState('')

  if (isLoading) return <p className="p-8">Loading...</p>

  return (
    <main className="p-8 max-w-lg mx-auto">
      <h1 className="text-2xl font-bold mb-6">Users</h1>

      <ul className="mb-8 space-y-2">
        {users?.map(u => (
          <li key={u.id} className="border rounded p-3">
            <p className="font-medium">{u.name}</p>
            <p className="text-sm text-muted-foreground">{u.email}</p>
          </li>
        ))}
      </ul>

      <form
        className="flex flex-col gap-3"
        onSubmit={e => {
          e.preventDefault()
          createUser.mutate({ name, email })
          setName(''); setEmail('')
        }}
      >
        <input className="border rounded px-3 py-2" placeholder="Name"
          value={name} onChange={e => setName(e.target.value)} />
        <input className="border rounded px-3 py-2" placeholder="Email"
          value={email} onChange={e => setEmail(e.target.value)} />
        <button type="submit"
          className="rounded bg-black text-white py-2 text-sm font-medium">
          Add User
        </button>
      </form>
    </main>
  )
}
10

Start the Development Server

Run the dev server. Hover over any tRPC call to see the inferred input/output types. Rename a field in your router — TypeScript will flag every caller instantly.

npm run dev
💡Navigate to http://localhost:3000/users. The `useTRPC()` hook gives you fully typed access to every procedure — no casting, no guessing.

Found this guide helpful? Check out more tools and guides.

Browse All Guides