Databases & ORMs
SQL, NoSQL, and Prisma ORM in Next.js applications
SQL vs NoSQL
Most production Next.js apps use PostgreSQL + Prisma. Serverless DB providers like Neon and PlanetScale scale to zero when idle — cost-effective for indie projects.
Prisma Setup
npm install prisma @prisma/client
npx prisma init
Environment variable in .env:
DATABASE_URL="postgresql://user:password@localhost:5432/myapp"
The generated schema.prisma:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
}
Queries in Server Components
Direct DB access without an API layer — one of Next.js's superpowers.
import { prisma } from "@/lib/prisma"
// lib/prisma.ts — singleton pattern prevents multiple instances
import { PrismaClient } from "@prisma/client"
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
// app/posts/page.tsx — Server Component queries DB directly
export default async function PostsPage() {
const posts = await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { name: true, email: true },
},
},
orderBy: { createdAt: "desc" },
take: 20,
})
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
</li>
))}
</ul>
)
}
Migrations & Seeding
# Create migration from schema changes
npx prisma migrate dev --name add-comment-model
# Apply to production
npx prisma migrate deploy
# Reset local DB
npx prisma migrate reset
Seed file — prisma/seed.ts:
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
async function main() {
const alice = await prisma.user.upsert({
where: { email: "alice@example.com" },
update: {},
create: {
email: "alice@example.com",
name: "Alice",
posts: {
create: [
{ title: "Getting Started with Prisma", published: true },
{ title: "Advanced Queries", published: false },
],
},
},
})
console.log("Seeded:", alice)
}
main()
.catch((e) => console.error(e))
.finally(() => prisma.$disconnect())
# Add to package.json
# "prisma": { "seed": "tsx prisma/seed.ts" }
npx prisma db seed
Relation Queries
// Include — fetch author with every post
const posts = await prisma.post.findMany({
include: { author: true },
})
// Select — pick specific fields, deeply
const user = await prisma.user.findUnique({
where: { email: "alice@example.com" },
select: {
name: true,
posts: {
where: { published: true },
select: { title: true, createdAt: true },
orderBy: { createdAt: "desc" },
},
},
})
// Nested create — user + post in one operation
const userWithPost = await prisma.user.create({
data: {
name: "Bob",
email: "bob@example.com",
posts: {
create: { title: "First Post", content: "Hello world" },
},
},
include: { posts: true },
})
Connection Pooling & Edge
Serverless functions are short-lived — every function gets its own DB connection. Pooling prevents connection exhaustion.
// lib/prisma.ts — for serverless (Neon, PlanetScale)
import { PrismaClient } from "@prisma/client"
import { PrismaNeonHTTP } from "@prisma/adapter-neon"
import { Pool } from "@neondatabase/serverless"
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaNeonHTTP(pool)
export const prisma = new PrismaClient({ adapter })
Edge Runtime does not support TCP connections. Use HTTP-based adapters (Neon HTTP, PlanetScale serverless driver) when deploying to Vercel Edge Functions or Cloudflare Workers.
See It In Action
1datasource db {2provider = "postgresql"3url = env("DATABASE_URL")4}5 6generator client {7provider = "prisma-client-js"8}9 10model User {11id String @id @default(cuid())12name String13email String @unique14posts Post[]15comments Comment[]16createdAt DateTime @default(now())17updatedAt DateTime @updatedAt18}19 20model Post {21id String @id @default(cuid())22title String23content String?24published Boolean @default(false)25author User @relation(fields: [authorId], references: [id])26authorId String27category Category? @relation(fields: [categoryId], references: [id])28categoryId String?29tags Tag[]30comments Comment[]31createdAt DateTime @default(now())32updatedAt DateTime @updatedAt33}34 35model Category {36id String @id @default(cuid())37name String38slug String @unique39posts Post[]40}41 42model Tag {43id String @id @default(cuid())44name String @unique45posts Post[]46}47 48model Comment {49id String @id @default(cuid())50content String51author User @relation(fields: [authorId], references: [id])52authorId String53post Post @relation(fields: [postId], references: [id])54postId String55createdAt DateTime @default(now())56}57 Complete blog schema with 5 models: User (one-to-many posts/comments), Post (belongs to User and Category, many-to-many Tags via implicit join table, has many Comments), Category (one-to-many posts), Tag (many-to-many posts), Comment (belongs to User and Post). Covers @id @default @unique @relation, optional fields, and timestamps.