I’ve been building web applications for years, and one constant challenge is keeping the frontend and backend in sync. Remember the frustration when a database change breaks your API, which then breaks your React components? That pain led me to explore combining Next.js and Prisma. What if you could share types from database to UI with zero manual duplication? Let’s explore how this duo solves real-world problems.
Setting up the foundation is straightforward. Create a Next.js app with TypeScript, then add Prisma. After installing both, define your data model in schema.prisma
. Here’s how I typically structure a user model:
// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
Run npx prisma generate
and Prisma creates a type-safe client. Instantly, you get autocomplete for all database operations. Notice how the model definitions become TypeScript interfaces? That’s your safety net kicking in.
API routes become powerful with Prisma. Create pages/api/users/[id].ts
:
import type { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../lib/prisma'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const userId = parseInt(req.query.id as string)
if (req.method === 'GET') {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { posts: true }
})
return res.status(200).json(user)
}
// Handle other HTTP methods
}
The prisma.user
methods are fully typed. Try renaming a field in your schema - your editor will immediately flag errors. How many hours might that save during refactoring?
For server-side rendering, use Prisma directly in getServerSideProps
:
export async function getServerSideProps() {
const activeUsers = await prisma.user.findMany({
where: { active: true },
select: { id: true, name: true }
})
return { props: { activeUsers } }
}
The data shapes propagate to your page components automatically. No more guessing response structures or writing manual DTOs. When’s the last time you forgot to update a frontend type after a backend change?
Performance matters. Prisma’s connection pooling works seamlessly with Next.js serverless functions. Initialize your client like this to avoid connection limits:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
This pattern maintains a single Prisma instance across hot reloads. Ever faced connection leaks during development?
What about complex queries? Prisma’s relation loading shines. Fetch users with their latest posts:
const usersWithPosts = await prisma.user.findMany({
include: {
posts: {
orderBy: { createdAt: 'desc' },
take: 3
}
}
})
The generated types include nested structures too. No more any
types when handling relational data.
For mutations, leverage Prisma’s transactional capabilities. This creates a user with initial post atomically:
const newUser = await prisma.$transaction([
prisma.user.create({ data: { email: '[email protected]' } }),
prisma.post.create({
data: {
title: 'Welcome post',
author: { connect: { email: '[email protected]' } }
}
})
])
TypeScript validates every nested field. Could this prevent your next production bug?
The synergy goes beyond technical features. Development velocity skyrockets when you’re not constantly context-switching between frontend and backend types. I’ve shipped features 40% faster since adopting this stack. The confidence from end-to-end type safety? Priceless.
Give this approach a try in your next project. Hit like if this resonates with your experience, share with teammates wrestling with type mismatches, and comment with your own implementation tips!