webdev.complete
🧬 ORMs: Prisma & Drizzle
🟢The Backend
Lesson 73 of 117
30 min

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

bash
npm install prisma @prisma/client
npx prisma init --datasource-provider postgresql

This creates a prisma/ folder with schema.prisma and a .env with DATABASE_URL.

The schema.prisma language

schema.prismais 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).

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

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).

The relation field is on the parent
Note 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, applies it, regenerates the client. 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.
bash
# 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 deploy

Generating the client (Prisma 7 is explicit)

Before Prisma 7, the client was regenerated automatically on npm install. As of Prisma 7 (mid-2025), you run it explicitly so your CI is deterministic:

bash
npx prisma generate
# Generates a fully-typed client into node_modules/.prisma/client

Add this to your postinstall script if you want the old behavior:

json
{
  "scripts": {
    "postinstall": "prisma generate"
  }
}

Using the client

users.service.ts
import { PrismaClient } from "@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.

lib/prisma.ts
import { PrismaClient } from "@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.

app/users/page.tsx
// 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>
  );
}
Connection pooling matters in serverless
Every serverless function instance opens its own connections. With 50 concurrent invocations, you can blow past Postgres's 100-connection default in seconds. Use Prisma Accelerate(Prisma's managed connection pooler + edge cache), or PgBouncer, or a database that pools natively like Neon or Supabase.

Prisma Studio

Prisma ships a GUI browser for your database. Run it locally to inspect rows, edit data, and explore relations without writing SQL.

bash
npx prisma studio
# Opens http://localhost:5555

When 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.
ts
// 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

Quiz1 / 4

Which command applies pending migrations safely in production?

Recap

  • Schema lives in prisma/schema.prisma. Models use @ field attributes and @@ model attributes.
  • migrate dev for local changes (interactive); migrate deploy for production (non-interactive).
  • prisma generate emits a typed client. Prisma 7 made it explicit - add it to postinstall if you miss the old auto-run.
  • Use a singleton via globalThis to avoid connection leaks during dev hot reload.
  • Prisma is Node-only; pair with Accelerate for connection pooling and edge access. Drop to $queryRaw for complex SQL.
  • npx prisma studio is the built-in DB browser.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.