Want to create a modern API that stays consistent from the database up to the browser? A type‑safe architecture guarantees that every layer communicates with the right contract, catching bugs early and making refactors painless. This guide shows you how to combine Next.js 13 App Router, tRPC, and Prisma for a robust, end‑to‑end type‑safe API.
Why Type‑Safety Matters in Full‑Stack APIs
Type‑safety reduces runtime errors by enforcing contracts across all communication channels. In a typical stack, mismatches between your database schema and API responses can surface only in production. With tRPC, your client can
import generated type definitions from the server, eliminating the need for autogenerated OpenAPI specs.
Definitions
- tRPC: A library that generates type‑safe APIs in TypeScript with RPC‑style endpoints.
- Prisma: An ORM that maps your database to strongly typed models and generates API clients.
- Next.js App Router: The new routing paradigm for Next.js 13 that supports file‑based routing and middleware.
What Makes an API Type‑Safe?
- All input and output types are explicitly typed.
- Client and server share the same type definitions.
- Database models are mapped to the API contracts without manual duplication.
- Validation catches malformed requests before reaching business logic.
Project Setup: Next.js 13 App Router + TypeScript
Start by bootstrapping a slim Next.js 13 project with TypeScript:
npx create-next-app@latest my-api --typescript
cd my-api
npm i
Move into the app directory and create a folder api for our server‑side logic. Next.js 13 treats files inside app/api as server functions.
Adding tRPC for End‑to‑End Type Safety
Install the required packages:
npm i @trpc/server @trpc/client @trpc/react @trpc/next zod
Configure tRPC in pages/_app.tsx (or app/layout.tsx for the new app router) to expose a server‑side router:
import type { AppRouter } from '../server/routers/_app'
import { withTRPC } from '@trpc/next'
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 default withTRPC({
config({ ctx }) {
return {
url: `${getBaseUrl()}/api/trpc`,
}
},
ssr: false,
})
Now, create server/routers/_app.ts and a sample router:
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
export const appRouter = t.router({
getUser: t.procedure
.input(z.string())
.query(async ({ input }) => {
// We'll wire this to Prisma later
return { id: input, name: 'Demo User' }
}),
})
export type AppRouter = typeof appRouter
Prisma Integration: Define Your Schema, Generate Client
Install Prisma and create an SQLite database for development:
npm i prisma @prisma/client
npx prisma init --datasource-provider sqlite
Open prisma/schema.prisma and define a simple User model:
datasource db {
provider = "sqlite"
url = env('DATABASE_URL')
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(uuid())
name String
email String @unique
}
Run the migration and generate the client.
npx prisma migrate dev --name init
Replace the placeholder tRPC query with real Prisma logic:
appRouter.getUser.query(async ({ input }) => {
const user = await prisma.user.findUnique({ where: { id: input } })
if (!user) throw new Error('User not found')
return user
})
Auth & Middleware: Secure the API Paths
Use a lightweight JWT middleware or next-auth for token verification. Here’s a quick example using next-auth with the App Router:
npm i next-auth
Configure next-auth in app/api/auth/[...nextauth]/route.ts and add a middleware in middleware.ts to protect API routes:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { getToken } from 'next-auth/jwt'
export async function middleware(req: NextRequest) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
if (!token) {
return NextResponse.redirect(new URL('/login', req.url))
}
return NextResponse.next()
}
export const config = {
matcher: [
"/api/:path*",
],
}
Testing & Validation: Zod + tRPC + Prisma
Validate incoming data with zod before hitting Prisma. For example, create an input schema for creating a user:
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
})
appRouter.createUser = t.procedure
.input(createUserSchema)
.mutation(async ({ input }) => {
return await prisma.user.create({ data: input })
})
In your client, call the procedure like:
const result = await trpc.createUser.mutate({ name: 'John', email: 'john@example.com' })
Deployment: Vercel or Docker
For a quick Vercel deployment:
- Commit your code to GitHub.
- Connect the repo to Vercel and set environment variables (
DATABASE_URL,NEXTAUTH_SECRET, etc.). - Vercel auto‑detects the Next.js app and builds.
If you prefer Docker:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm ci
COPY . .
RUN npx prisma generate
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Conclusion
By chaining together Next.js 13 App Router, tRPC, and Prisma, you can build a seamless, type‑safe API that spans from the database to the browser. This pattern cuts down on runtime surprises, speeds up refactor cycles, and delivers a crystal‑clear contract for your frontend developers. Build your first route today, add authentication, and deploy to Vercel to see the power of type safety in action.