Building Type-Safe APIs with tRPC and Next.js
TypeScriptNext.jsAPI

Building Type-Safe APIs with tRPC and Next.js

August 18, 20257 min read

The API contract problem is one of those things that seems solved until it isn't. You write a REST endpoint, define the response shape in your head, consume it on the frontend, and everything works - until someone changes the backend and the frontend silently breaks at runtime.

tRPC takes a different approach: it makes the contract impossible to violate at compile time.

What tRPC Actually Does

tRPC doesn't generate code. There's no openapi.yaml, no Swagger spec, no separate schema file. Instead, you write regular TypeScript functions on the server and call them directly from the client - with full type inference flowing through.

The trick is that tRPC infers the input and output types of your router procedures, and exposes them to the client through a shared type. No network call needed to get the types - TypeScript figures it out statically.

// server/router.ts
import { z } from 'zod'
import { router, publicProcedure } from './trpc'

export const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return db.user.findUnique({ where: { id: input.id } })
    }),
})

export type AppRouter = typeof appRouter

On the client:

// components/UserCard.tsx
const { data: user } = trpc.getUser.useQuery({ id: userId })
// user is typed as User | null,no manual typing required

If you rename getUser on the server, TypeScript immediately flags every usage on the client. If you change the return shape, every consumer breaks at compile time. The feedback loop that used to require a runtime error in production now happens in your editor.

Setting Up tRPC in Next.js App Router

The App Router integration requires a bit more setup than the Pages Router, but it's worth it.

First, install the packages:

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

Create your tRPC server instance:

// server/trpc.ts
import { initTRPC } from '@trpc/server'
import { cache } from 'react'

const createTRPCContext = cache(async () => ({
  // your context here,db connections, auth session, etc.
}))

const t = initTRPC.context<typeof createTRPCContext>().create()

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

Wire up the API route handler:

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/server/router'

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

export { handler as GET, handler as POST }

When to Use tRPC vs REST

tRPC shines when both the client and server are TypeScript and both live in the same repo (or monorepo). The zero-codegen DX is genuinely excellent,you add a procedure, use it on the client, done.

Where REST still makes sense: public APIs consumed by external clients, or when your backend is polyglot. tRPC is a TypeScript-to-TypeScript transport layer,that's both its superpower and its constraint.

After using it across four different projects, my default for full-stack Next.js work is now tRPC unless there's a specific reason for REST. The type safety dividends compound over time, especially when the team grows and people are modifying endpoints they didn't originally write.


The next evolution from here is tRPC with Zod coercion for complex input validation. But that's a post for another day.

Continue Reading

Related Articles

The Mental Model I Use for React Server Components
Featured
ReactNext.jsPerformance

The Mental Model I Use for React Server Components

After six months of production RSC usage, one reframe made everything click: stop thinking about components and start thinking about render boundaries.

Dec 20, 20258 min read
Zero to Production: Deploying Next.js on Railway
DevOpsNext.jsDeployment

Zero to Production: Deploying Next.js on Railway

Railway removed all the reasons I had to use Vercel. Here's a complete walkthrough including environment variables, Postgres, and custom domains.

Nov 22, 20257 min read