Prisma
Schema language, migrate, generate, queries. Prisma Studio.
Prisma is the most-installed Node ORM in 2026. Its pitch: write your database schema in a clean DSL, run one command, get a fully-typed client that knows every table, every column, and every relation. Most teams pick it because the migration tooling is genuinely the best in the ecosystem and the autocomplete is hard to beat. Let's build with it.
Setup
npm install prisma @prisma/client @prisma/adapter-pg
npx prisma init --datasource-provider postgresqlThis creates a prisma/ folder with schema.prisma and a .env with DATABASE_URL.
- The package is ESM only. You need
"type": "module". - Minimum Node 20.19 and TypeScript 5.4.
- The generator provider is
prisma-client, andoutputis required. Nothing is generated intonode_modulesany more, so you import from your own path, not@prisma/client. - Connection config moved to a
prisma.config.tsat the project root.urlin thedatasourceblock is deprecated. - Driver adapters are required for every database (
@prisma/adapter-pgand friends), so pool defaults now come from the underlying driver rather than Prisma. - Env vars are no longer loaded for you. Call
dotenvyourself.
$use middleware and the metrics API were removed outright; use Client Extensions instead.The schema.prisma language
schema.prisma is Prisma's own DSL. Think of it as TypeScript's younger cousin for describing databases. Three things live in it: the generator (which language to emit a client for), the datasource (which DB to talk to), and your models (tables).
generator client {
provider = "prisma-client" // not "prisma-client-js" any more
output = "../src/generated/prisma" // required in Prisma 7
}
datasource db {
provider = "postgresql"
}
model User {
id String @id @default(uuid())
email String @unique
name String
createdAt DateTime @default(now())
orders Order[]
}
model Order {
id String @id @default(uuid())
userId String
amountCents Int @db.Integer
status String @default("pending")
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, createdAt(sort: Desc)])
}Field-level attributes start with @ (e.g. @id, @default, @unique). Model-level attributes start with @@ (e.g. @@index, @@map).
orders Order[] on User and user User @relation(...) on Order. Prisma infers the foreign-key constraint from the @relation side. The plural array side is purely a query convenience and adds no DB columns.Migrations: dev vs deploy
Prisma generates SQL migration files from changes to your schema. Two commands you need to know:
npx prisma migrate dev --name init- in development: generates a migration and applies it. Interactive, and may reset the DB if you have drift.npx prisma migrate deploy- in production/CI: applies pending migrations only. Non-interactive, safe for automation.
migrate dev also regenerated the client and ran your seed script. Prisma 7 does neither, and it removed the --skip-generate and --skip-seed flags that used to turn that off. Run prisma generate and prisma db seed yourself, or chain them in an npm script. The symptom when you forget is a type error on a field you just added.# 1. Edit schema.prisma
# 2. Generate + apply migration locally
npx prisma migrate dev --name add_orders_table
# This creates prisma/migrations/20260524103011_add_orders_table/migration.sql
# and applies it to your dev DB.
# In CI / production:
npx prisma migrate deployGenerating the client
Older Prisma regenerated the client automatically on npm install. Now you run it, which makes CI deterministic:
npx prisma generate
# Writes a typed client to the "output" path in your generator block,
# e.g. src/generated/prisma. Commit it or gitignore it, but be consistent.Add this to your postinstall script if you want the old behavior:
{
"scripts": {
"postinstall": "prisma generate"
}
}Using the client
import { PrismaClient } from "../src/generated/prisma/client";
const prisma = new PrismaClient();
// Create
const user = await prisma.user.create({
data: { email: "ada@example.com", name: "Ada Lovelace" },
});
// Read - find by unique field
const same = await prisma.user.findUnique({
where: { email: "ada@example.com" },
});
// Read - filter and include relations
const usersWithOrders = await prisma.user.findMany({
where: { name: { contains: "Ada", mode: "insensitive" } },
include: { orders: { orderBy: { createdAt: "desc" }, take: 5 } },
orderBy: { createdAt: "desc" },
take: 20,
});
// Update
const updated = await prisma.user.update({
where: { id: user.id },
data: { name: "Ada King" },
});
// Delete
await prisma.user.delete({ where: { id: user.id } });
// Aggregate
const totals = await prisma.order.groupBy({
by: ["userId"],
_sum: { amountCents: true },
_count: true,
});The singleton pattern for hot reload
In dev mode with Next.js (or any hot-reloading framework), modules reload constantly. If you do new PrismaClient() at the top of a file, each reload opens new database connections until your DB hits its connection limit. The fix: stash the client on globalThis.
import { PrismaClient } from "../src/generated/prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"],
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}Then every file does import { prisma } from "@/lib/prisma". One client, no leaks, hot reload survives.
Using Prisma in Server Components
Prisma is a Node-only library (it ships native binaries). In Next.js App Router, that means it works in Server Components, Route Handlers, and Server Actions, but not in Edge runtime or client components.
// Server Component - runs on the Node server
import { prisma } from "@/lib/prisma";
export default async function UsersPage() {
const users = await prisma.user.findMany({
orderBy: { createdAt: "desc" },
take: 20,
});
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}Prisma Studio
Prisma ships a GUI browser for your database. Run it locally to inspect rows, edit data, and explore relations without writing SQL.
npx prisma studio
# Opens http://localhost:5555When Prisma stops being magic
Two cases where you'll feel the limits:
- Complex queries - window functions, CTEs, heavy analytics. Drop to raw SQL with
prisma.$queryRaw. - Edge runtime - Prisma needs Node. Use Accelerate (HTTP-based) or switch to Drizzle if your whole stack lives at the edge.
// Raw SQL when you need it - still type-safe via generics
const rows = await prisma.$queryRaw`
SELECT u.id, u.name, COUNT(o.id) AS order_count
FROM "User" u
LEFT JOIN "Order" o ON o.user_id = u.id
GROUP BY u.id, u.name
HAVING COUNT(o.id) >= ${10}
`;Quiz
Which command applies pending migrations safely in production?
Recap
- Schema lives in
prisma/schema.prisma. Models use@field attributes and@@model attributes. migrate devfor local changes (interactive);migrate deployfor production (non-interactive).prisma generateemits a typed client. Prisma 7 made it explicit - add it topostinstallif you miss the old auto-run.- Use a singleton via
globalThisto avoid connection leaks during dev hot reload. - Prisma is Node-only; pair with Accelerate for connection pooling and edge access. Drop to
$queryRawfor complex SQL. npx prisma studiois the built-in DB browser.