Overview
Prisma 7 is a TypeScript-first ORM that generates a typed client from a single schema.prisma file. It is the default ORM for Node and Bun services backed by postgres or sqlite. Prisma 7 (released 2025-11-19) ships the Rust-free prisma-client generator as the default and requires a driver adapter to connect. This page covers schema modeling, the migration workflow, transactions, connection pooling, and when to step outside the query builder. To move an existing project off Prisma 5 or 6, see prisma-v6-to-v7-upgrade.
Model the schema explicitly
schema.prisma is the source of truth. Every table, column, index, and relation lives there.
model User {
id String @id @default(cuid())
email String @unique
createdAt DateTime @default(now())
orders Order[]
@@index([createdAt])
}
model Order {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
total Int
@@index([userId, id])
}- Use
@idon every model. Prefercuid()oruuid(7)over auto-incrementing ints for distributed systems. On postgres 18, time-ordereduuidv7()keys index better than random v4 UUIDs. - Use
@uniquefor natural keys you query by (email, slug). Add@@unique([a, b])for composite uniqueness. - Declare relations on both sides.
onDeleteis explicit and matches the postgresFOREIGN KEYclause. - Add
@@indexfor every query that filters or sorts on a non-key column.
Use migrate dev locally and migrate deploy in production
The two commands serve different masters.
prisma migrate devcreates a new SQL migration from your schema diff, runs it locally, and regenerates the client. Use during development.prisma migrate deployapplies pending migrations in order without prompting. Use in CI and production.- Never edit a migration that has shipped. Write a follow-up migration. See postgres for the migration contract.
- Commit the generated SQL file along with the schema change. Reviewers read the SQL, not the diff of
schema.prismaalone.
Wrap multi-step writes in $transaction
Atomicity is opt-in. Group writes that must succeed or fail together.
await prisma.$transaction([
prisma.order.create({ data: { userId, total } }),
prisma.inventory.update({
where: { sku },
data: { stock: { decrement: 1 } },
}),
]);Use the interactive form for read-modify-write logic:
await prisma.$transaction(async (tx) => {
const item = await tx.inventory.findUnique({ where: { sku } });
if (!item || item.stock < 1) throw new Error("out of stock");
await tx.inventory.update({
where: { sku },
data: { stock: { decrement: 1 } },
});
await tx.order.create({ data: { userId, total, sku } });
});Set isolationLevel: "Serializable" when concurrent writers can race on the same rows.
Pool with PgBouncer or Prisma Accelerate
A single Prisma client opens one connection per concurrent query. Serverless and edge workloads multiply that fast.
- Long-running Node servers: PgBouncer in transaction mode in front of Postgres. Append
?pgbouncer=true&connection_limit=1toDATABASE_URL. - Serverless or edge: Prisma Accelerate or a dedicated pooler (Supabase, Neon). Direct connections from Lambda or Workers will exhaust Postgres under load.
- See postgres for the PgBouncer sizing rules.
Trust the generated client types
The generated client is the API contract. In Prisma 7 the prisma-client generator emits it to an explicit output directory in your source tree (for example src/generated/prisma), not node_modules. You import PrismaClient from that path and pass a driver adapter when you construct it.
import { PrismaClient } from "./generated/prisma/client"
import { PrismaPg } from "@prisma/adapter-pg"
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
export const prisma = new PrismaClient({ adapter })- Run
prisma generateafter every schema change. Wire it intopostinstallso CI never ships a stale client. - Use
Prisma.UserGetPayload<{ include: { orders: true } }>to derive return types instead of writing them by hand. - Keep schema and client in sync across services that share a database. Drift produces silent runtime errors.
- See prisma-driver-adapters for adapter selection and prisma-client for the singleton pattern.
Drop to $queryRaw when the query builder is in the way
The query builder is excellent for CRUD. It is poor at recursive CTEs, window functions, JSONB path queries, full-text search, and any plan you need to hand-tune.
const rows = await prisma.$queryRaw<Array<{ id: string; rank: number }>>`
SELECT id, ts_rank(search_tsv, plainto_tsquery(${q})) AS rank
FROM articles
WHERE search_tsv @@ plainto_tsquery(${q})
ORDER BY rank DESC
LIMIT 20;
`;Use the tagged template form. It parameterizes values and prevents injection. Reach for $executeRaw for writes that the builder cannot express.