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 @id on every model. Prefer cuid() or uuid(7) over auto-incrementing ints for distributed systems. On postgres 18, time-ordered uuidv7() keys index better than random v4 UUIDs.
  • Use @unique for natural keys you query by (email, slug). Add @@unique([a, b]) for composite uniqueness.
  • Declare relations on both sides. onDelete is explicit and matches the postgres FOREIGN KEY clause.
  • Add @@index for 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 dev creates a new SQL migration from your schema diff, runs it locally, and regenerates the client. Use during development.
  • prisma migrate deploy applies 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.prisma alone.

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=1 to DATABASE_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 generate after every schema change. Wire it into postinstall so 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.