advanced
Step 13 of 15
Database Integration
Next.js Development
Database Integration with Prisma
Next.js applications often need persistent data storage, and Prisma has become the most popular ORM for the TypeScript/JavaScript ecosystem. Prisma provides a type-safe database client generated from your schema, automatic migrations, and an intuitive query API. In Next.js, database operations run exclusively on the server — in Server Components, API routes, and Server Actions. Prisma works with PostgreSQL, MySQL, SQLite, MongoDB, and other databases. Its type safety means that query results are fully typed and invalid queries are caught at compile time.
Setting Up Prisma
# Install Prisma
npm install prisma --save-dev
npm install @prisma/client
# Initialize Prisma with SQLite for development
npx prisma init --datasource-provider sqlite
# This creates:
# prisma/schema.prisma — database schema
# .env — database connection string
Defining the Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite" // Use "postgresql" for production
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
role String @default("viewer")
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
slug String @unique
body String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
# Run migrations
npx prisma migrate dev --name init
# Generate the Prisma Client
npx prisma generate
# Open Prisma Studio (GUI for your database)
npx prisma studio
Database Client Singleton
// lib/prisma.ts — singleton pattern for Next.js
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query"] : [],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;
Using Prisma in Server Components
// app/blog/page.tsx — fetch posts directly in Server Component
import prisma from "@/lib/prisma";
import Link from "next/link";
export default async function BlogPage() {
const posts = await prisma.post.findMany({
where: { published: true },
include: {
author: { select: { name: true } },
tags: { select: { name: true } },
},
orderBy: { createdAt: "desc" },
take: 20,
});
return (
<div>
<h1>Blog</h1>
{posts.map((post) => (
<article key={post.id}>
<Link href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
</Link>
<p>By {post.author.name}</p>
<div className="tags">
{post.tags.map((tag) => (
<span key={tag.name} className="tag">{tag.name}</span>
))}
</div>
</article>
))}
</div>
);
}
CRUD API Routes with Prisma
// app/api/posts/route.ts
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "10");
const [posts, total] = await Promise.all([
prisma.post.findMany({
where: { published: true },
include: { author: { select: { name: true, email: true } } },
orderBy: { createdAt: "desc" },
skip: (page - 1) * limit,
take: limit,
}),
prisma.post.count({ where: { published: true } }),
]);
return NextResponse.json({
data: posts,
meta: { page, limit, total, pages: Math.ceil(total / limit) },
});
}
export async function POST(request: Request) {
try {
const { title, body, authorId, tags } = await request.json();
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
const post = await prisma.post.create({
data: {
title,
slug,
body,
authorId,
tags: {
connectOrCreate: tags.map((tag: string) => ({
where: { name: tag },
create: { name: tag },
})),
},
},
include: { author: true, tags: true },
});
return NextResponse.json(post, { status: 201 });
} catch (error: any) {
if (error.code === "P2002") {
return NextResponse.json(
{ error: "A post with this slug already exists" },
{ status: 409 }
);
}
return NextResponse.json({ error: "Server error" }, { status: 500 });
}
}
Tip: Always use the Prisma client singleton pattern in Next.js to prevent creating too many database connections during development hot reloads. In production, the singleton is created once. Use prisma.post.findMany in Server Components for direct database access without an API layer.
Key Takeaways
- Prisma provides a type-safe ORM with auto-generated client, migrations, and an intuitive query API.
- Use the singleton pattern for the Prisma client to avoid connection leaks in development.
- Query the database directly in Server Components — no API route needed for read operations.
- Use API routes for mutations (create, update, delete) that client components trigger.
- Prisma supports relations, pagination, filtering, and transactions for complex data operations.