tRPC
Add end-to-end type-safe APIs to any frontend — Next.js, TanStack Start, or Vite React — no schemas, no code generation, just TypeScript.
Overview
tRPC lets you build fully type-safe APIs without a schema language or code generation step. Your router is defined in TypeScript and the client infers all types automatically — if you rename a procedure or change its input, TypeScript will catch the mismatch instantly.
Fusion Stack scaffolds a complete tRPC v11 setup tailored to your chosen frontend and backend.
Scaffolded Structure
The file paths vary by frontend:
Next.js (default src/ layout — drop src/ prefix with --no-src-dir):
Note: Paths below use the default
src/layout. If you chose the root layout (--no-src-dir), drop thesrc/prefix — e.g.server/instead ofsrc/server/.
src/
server/
trpc.ts # initTRPC — exports router and publicProcedure
routers/
index.ts # Root appRouter with a sample hello procedure
trpc-hono.ts # Hono adapter helper (only used when backend = Hono)
app/
api/
trpc/
[trpc]/
route.ts # Next.js App Router handler
lib/
trpc/
client.ts # Vanilla tRPC client (server components / server actions)
react.ts # createTRPCReact hook factory
provider.tsx # TRPCProvider — wraps QueryClient + tRPC clientSetup
1. Wrap your root layout
Add TRPCProvider to src/app/layout.tsx:
import { TRPCProvider } from '@/lib/trpc/provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<TRPCProvider>{children}</TRPCProvider>
</body>
</html>
)
}2. Call procedures from React components
Use the generated React hooks to call your procedures:
'use client'
import { trpcReact } from '@/lib/trpc/react'
export function Hello() {
const { data } = trpcReact.hello.useQuery({ name: 'fusion-stack' })
return <p>{data?.greeting}</p>
}3. Call from Server Components
Use the vanilla client for server-side calls:
import { trpc } from '@/lib/trpc/client'
export default async function Page() {
const data = await trpc.hello.query({ name: 'world' })
return <p>{data.greeting}</p>
}Adding Procedures
Define new procedures in src/server/routers/index.ts:
import { z } from 'zod'
import { router, publicProcedure } from '../trpc'
export const appRouter = router({
hello: publicProcedure
.input(z.object({ name: z.string().optional() }))
.query(({ input }) => {
return { greeting: `Hello, ${input.name ?? 'world'}!` }
}),
createPost: publicProcedure
.input(z.object({ title: z.string(), body: z.string() }))
.mutation(async ({ input }) => {
const post = await db.post.create({ data: input })
return post
}),
})
export type AppRouter = typeof appRouterHono Backend
When your backend is Hono, mount tRPC on the Hono server instead of (or in addition to) the Next.js route handler:
// src/server/index.ts
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { trpcHonoHandler } from './trpc-hono'
const app = new Hono()
app.get('/health', (c) => c.json({ status: 'ok' }))
app.use('/trpc/*', trpcHonoHandler)
serve({ fetch: app.fetch, port: 3001 })Then update the tRPC client URL to point at the Hono server:
// src/lib/trpc/client.ts
httpBatchLink({ url: 'http://localhost:3001/trpc', transformer: superjson })Adding Authentication Context
Pass auth context to procedures via createContext:
// src/app/api/trpc/[trpc]/route.ts
import { auth } from '@clerk/nextjs/server'
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: async () => {
const { userId } = await auth()
return { userId }
},
})Access it in procedures:
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' })
return next({ ctx: { userId: ctx.userId } })
})TanStack Start
When your frontend is TanStack Start, tRPC mounts via createAPIFileRoute — no separate server needed:
// app/routes/api/trpc.ts
import { createAPIFileRoute } from "@tanstack/start/api"
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"
import { appRouter } from "../../server/routers/index"
export const APIRoute = createAPIFileRoute("/api/trpc/$")({
GET: ({ request }) => fetchRequestHandler({ endpoint: "/api/trpc", req: request, router: appRouter, createContext: () => ({}) }),
POST: ({ request }) => fetchRequestHandler({ endpoint: "/api/trpc", req: request, router: appRouter, createContext: () => ({}) }),
})Wrap app/routes/__root.tsx with TRPCProvider from @/app/lib/trpc/provider.
Vite React
When your frontend is Vite React, tRPC is served via the Hono backend on port :3001. Mount the handler in src/server/index.ts:
import { trpcHonoHandler } from "./trpc-hono"
app.use("/trpc/*", trpcHonoHandler)Wrap src/main.tsx with TRPCProvider from @/lib/trpc/provider.
Compatibility
- ✅ Works with: Next.js API Routes, TanStack Start (built-in), Hono, Clerk, WorkOS, Better Auth, shadcn/ui
- ❌ Not compatible with: Convex backend (Convex has its own type-safe RPC system)