What Is tRPC?
tRPC (TypeScript Remote Procedure Call) lets you build APIs where the server and client share types automatically — no OpenAPI spec, no code generation, no schema files. If you rename a function on the server, TypeScript immediately tells every client that calls it.
tRPC works by exporting the router's type from the server and importing it on the client. No runtime overhead, no bundled schema — just type inference.
Setting Up tRPC with Next.js
# Install
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
// server/trpc.ts — initialize tRPC
import { initTRPC } from '@trpc/server'
const t = initTRPC.create()
export const router = t.router
export const publicProcedure = t.procedure
Defining Procedures
// server/routers/posts.ts
import { z } from 'zod'
import { router, publicProcedure } from '../trpc'
export const postsRouter = router({
list: publicProcedure
.input(z.object({ limit: z.number().min(1).max(100).default(10) }))
.query(async ({ input }) => {
return db.query.posts.findMany({ limit: input.limit })
}),
byId: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, input.id),
})
if (!post) throw new TRPCError({ code: 'NOT_FOUND' })
return post
}),
create: publicProcedure
.input(z.object({ title: z.string().min(1), body: z.string() }))
.mutation(async ({ input, ctx }) => {
return db.insert(posts).values({ ...input, userId: ctx.user.id }).returning()
}),
})
Using tRPC on the Client
// In a React component
import { trpc } from '../utils/trpc'
function PostList() {
// Fully typed — TypeScript knows the shape of data
const { data, isLoading } = trpc.posts.list.useQuery({ limit: 20 })
const createPost = trpc.posts.create.useMutation({
onSuccess: () => trpc.useUtils().posts.list.invalidate(),
})
if (isLoading) return <div>Loading...</div>
return (
<div>
{data?.map(post => <div key={post.id}>{post.title}</div>)}
<button onClick={() => createPost.mutate({ title: 'New Post', body: '' })}>
Create Post
</button>
</div>
)
}
Authentication with Context
// server/context.ts
import { getServerSession } from 'next-auth'
export async function createContext({ req, res }: CreateNextContextOptions) {
const session = await getServerSession(req, res, authOptions)
return { session, user: session?.user ?? null }
}
// Protected procedure
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' })
return next({ ctx: { user: ctx.user } }) // user is non-null here
})
Frequently Asked Questions
Can tRPC replace REST for a public API?
tRPC is designed for full-stack TypeScript monorepos — both client and server share the same codebase. For a public API consumed by non-TypeScript clients, REST or GraphQL with an OpenAPI spec is more appropriate.
How does tRPC handle subscriptions?
tRPC supports real-time subscriptions via WebSockets using the subscriptions procedure type and @trpc/server/adapters/ws. The client uses useSubscription to listen for events.
Is tRPC compatible with Zod v4?
tRPC v11 supports Zod v4. If you're on tRPC v10, stick with Zod v3. Both versions are actively maintained.