A practical, end‑to‑end guide for expert developers who are new to Prisma. Covers modeling, migrations, querying, performance, deployment, testing, and gotchas — with copy‑pasteable snippets.
0) What Prisma is (and isn’t)
Prisma is a modern TypeScript ORM + tooling that gives you:
- Prisma Client – a type‑safe query builder generated from your schema
- Prisma Migrate – schema‑driven migrations and drift detection
- Prisma Studio – a visual data browser
- Optional extras – e.g. Accelerate (global connection pooling / edge), adapters for serverless/edge runtimes
It is not a query language of its own (it compiles to SQL or Mongo queries under the hood) and it’s not a data layer framework like GraphQL.
Supported databases include PostgreSQL, MySQL / MariaDB, SQLite, SQL Server, CockroachDB, and MongoDB (special connector with a few differences).
1) Quickstart (greenfield)
# 1) Install
npm i -D prisma
npm i @prisma/client
# 2) Bootstrap project files
npx prisma init
# creates: prisma/schema.prisma and .env
# 3) Model your data in prisma/schema.prisma (see §2–§4)
# 4) Create your first migration & generate client
npx prisma migrate dev --name init
# (re)generates Prisma Client too
# 5) Open a data GUI
npx prisma studio
Create and use a client instance:
// src/db.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({ log: ["warn", "error"] });
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
2) Anatomy of schema.prisma
// prisma/schema.prisma
datasource db {
provider = "postgresql" // mysql | sqlite | sqlserver | mongodb | cockroachdb
url = env("DATABASE_URL")
// For advanced setups you can add:
// directUrl = env("DIRECT_URL")
// shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
// previewFeatures = ["..."] // opt-in features (only when needed)
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
email String @unique
name String?
role Role @default(USER)
posts Post[]
// Map to existing DB column names if needed:
// @map("user_name") and @@map("tbl_user") on the model
}
model Post {
id Int @id @default(autoincrement())
slug String @unique
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags Tag[] // implicit many-to-many via join table `_PostToTag`
@@index([authorId])
@@fulltext([title, content]) // Postgres/MySQL where supported
}
enum Role {
USER
ADMIN
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
Sections
-
datasource
: DB type + connection URL(s) -
generator
: which client(s) to generate (JS is default); can add extras via client extensions - Data model: your tables/collections; relations, indexes, defaults, enums, etc.
Common scalar types: String
, Int
, BigInt
, Float
, Decimal
, Boolean
, DateTime
, Bytes
, Json
(+ MongoDB’s ObjectId
via @db.ObjectId
).
Attributes you’ll use a lot
- IDs:
@id
, composite@@id([a,b])
,@default(autoincrement()) | cuid() | uuid()
- Uniqueness & indexes:
@unique
,@@unique([a,b])
,@@index([...])
,@@fulltext([...])
- Relations:
@relation(fields: [fk], references: [pk], onDelete: Cascade|Restrict|SetNull|NoAction, onUpdate: ...)
- Mapping:
@map("col")
,@@map("table")
to bind to legacy names
3) Relations by example
1‑to‑Many
model User { id Int @id @default(autoincrement()) posts Post[] }
model Post { id Int @id @default(autoincrement()) authorId Int author User @relation(fields: [authorId], references: [id]) }
1‑to‑1 (required on both sides)
model User { id Int @id @default(autoincrement()) profile Profile? }
model Profile { id Int @id @default(autoincrement()) userId Int @unique user User @relation(fields: [userId], references: [id]) }
Many‑to‑Many (implicit)
model Post { id Int @id @default(autoincrement()) tags Tag[] }
model Tag { id Int @id @default(autoincrement()) posts Post[] }
Prisma creates a hidden join table
_PostToTag
. Use an explicit join model if you need extra columns (e.g.,assignedAt
,weight
).
Referential actions
@relation(onDelete: Cascade) // delete children when parent is deleted
@relation(onDelete: SetNull) // set FK to NULL
4) Migrations: the definitive workflow
Local/dev
# Create a migration from schema changes, apply it, and regen client
npx prisma migrate dev --name add_posts
# Reset database and reapply all migrations (DANGER: data loss)
npx prisma migrate reset
Deploy/CI
# In pipelines, only apply checked-in migrations
npx prisma migrate deploy
When to use db push
(no migration files)
npx prisma db push # quick prototyping or sync to a dev DB without versioned SQL
Prefer migrate for team/CI. Use db push for prototypes or MongoDB.
Folders/files
-
prisma/migrations/2025xxxx_add_posts/
with SQL +migration.sql
-
_prisma_migrations
table records history - Shadow DB (optional) is used to safely diff schemas
Fixing mistakes
- If a migration failed in prod, create a follow‑up migration (never edit an already‑deployed migration)
- For complex drift, generate SQL with
prisma migrate diff
and apply manually
5) Introspection (brownfield)
Already have a DB? Generate a schema from it:
npx prisma db pull
Then you can start querying immediately. Going forward, choose either migrations or keep pulling and treat the DB as the source of truth (avoid mixing both in the same branch).
6) Seeding
// package.json
{
"prisma": { "seed": "ts-node prisma/seed.ts" }
}
// prisma/seed.ts
import { prisma } from "../src/db";
async function run() {
await prisma.user.create({
data: {
email: "admin@example.com",
role: "ADMIN",
posts: { create: { title: "Hello Prisma", slug: "hello-prisma" } },
},
});
}
run().finally(() => prisma.$disconnect());
npx prisma db seed
7) Querying with Prisma Client (the 95% you use daily)
CRUD basics
// Create
await prisma.post.create({ data: { title: "T1", slug: "t1", authorId: 1 } });
// Read
const post = await prisma.post.findUnique({ where: { id: 1 } });
// Update
await prisma.post.update({ where: { id: 1 }, data: { published: true } });
// Delete
await prisma.post.delete({ where: { id: 1 } });
Filtering, selecting, including
const posts = await prisma.post.findMany({
where: {
published: true,
title: { contains: "prisma", mode: "insensitive" },
author: { email: { endsWith: "@example.com" } },
},
select: { id: true, title: true, author: { select: { id: true, email: true } } },
orderBy: { createdAt: "desc" },
take: 20,
skip: 0,
});
Nested writes
// Create a post and connect to (or create) a tag in one go
await prisma.post.create({
data: {
title: "Nested",
slug: "nested",
author: { connect: { id: 1 } },
tags: {
connectOrCreate: [{ where: { name: "ts" }, create: { name: "ts" } }],
},
},
});
Upsert & bulk ops
await prisma.user.upsert({
where: { email: "a@b.com" },
create: { email: "a@b.com" },
update: { name: "Alice" },
});
await prisma.post.createMany({
data: [{ title: "a", slug: "a", authorId: 1 }, { title: "b", slug: "b", authorId: 1 }],
skipDuplicates: true,
});
Aggregation & grouping
const count = await prisma.post.count({ where: { published: true } });
const stats = await prisma.post.aggregate({ _avg: { id: true }, _max: { createdAt: true } });
const byAuthor = await prisma.post.groupBy({ by: ["authorId"], _count: true, where: { published: true } });
Cursor pagination
const page = await prisma.post.findMany({
cursor: { id: lastSeenId },
skip: 1,
take: 20,
orderBy: { id: "asc" },
});
Transactions
// Batched (runs in a single transaction)
await prisma.$transaction([
prisma.user.create({ data: { email: "x@y.com" } }),
prisma.post.create({ data: { title: "T", slug: "t", authorId: 1 } }),
]);
// Interactive transaction
await prisma.$transaction(async (tx) => {
const u = await tx.user.create({ data: { email: "z@z.com" } });
await tx.profile.create({ data: { userId: u.id } });
});
Raw SQL (only when you must)
import { Prisma } from "@prisma/client";
const rows = await prisma.$queryRaw<{ id: number }[]>`
SELECT id FROM "Post" WHERE title ILIKE ${"%prisma%"}
`;
Always pass values as template parameters (not string‑interpolated) to avoid SQL injection.
8) Middleware & Client Extensions
Middleware ($use
)
prisma.$use(async (params, next) => {
const started = Date.now();
const result = await next(params);
const took = Date.now() - started;
if (took > 200) console.warn(`[slow] ${params.model}.${params.action} in ${took}ms`);
return result;
});
Client extensions ($extends
)
Add reusable helpers or computed fields.
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient().$extends({
model: {
post: {
async publish(id: number) {
return prisma.post.update({ where: { id }, data: { published: true } });
},
},
},
result: {
post: {
preview: {
needs: { content: true },
compute(p) { return (p.content ?? "").slice(0, 120); },
},
},
},
});
await prisma.post.publish(1);
9) Deployment patterns & connection management
Long‑running servers (Express/Nest/Fastify)
- Create one
PrismaClient
per process and reuse it - Enable logs in dev, disable chatty logs in prod
Serverless (Vercel/Netlify/Lambda)
- Use a global singleton pattern to avoid opening a new pool per cold start
- Prefer a connection pooler (e.g. PgBouncer) or a serverless‑friendly provider (Neon, PlanetScale, Supabase pooler)
- Consider Prisma Accelerate or the Edge client when running at the edge
Edge runtimes
- Use
@prisma/client/edge
plus an adapter/Accelerate; keep queries lean and avoid long transactions
Helpful env vars
-
DATABASE_URL
– main connection string -
DIRECT_URL
– bypass pooler for migrations if needed -
SHADOW_DATABASE_URL
– migration shadow DB
10) Performance checklist
-
Modeling: add
@@index
, composite indexes for hot filters; avoid SELECT * (useselect
) -
N+1: load relations via
include
or do two queries with IDs andwhere: { id: { in: ids } }
-
Bulk ops: prefer
createMany
,updateMany
,deleteMany
where applicable - Pagination: use cursor pagination for infinite scroll
- Pooling: configure poolers; avoid creating clients in hot code paths
- Tracing/metrics: enable OpenTelemetry or built‑in logging in non‑prod to find slow spots
11) Testing strategies
- Run migrations against a throwaway DB per test run (Docker or file‑based SQLite)
- Reset via
prisma migrate reset
(or wrap tests in a top‑level transaction and roll back) - Provide factories using Prisma Client (and keep them fast)
12) Multi‑tenancy options
- Row‑level (tenantId column + RLS in Postgres) — simplest operationally
- Schema‑per‑tenant (Postgres) — isolation at schema level; more complex migrations
- Database‑per‑tenant — strongest isolation but operational overhead
13) Security notes
- Never interpolate raw SQL — always parameterize with
$queryRaw
template args - Enforce authorization in code (Prisma does not do auth) and/or DB policies (e.g., Postgres RLS)
- Validate inputs (Zod/Valibot) before querying
- Keep migrations in VCS; track who can run
migrate deploy
14) CLI command reference (most used)
# Project
npx prisma init # bootstrap schema + .env
npx prisma generate # regenerate Prisma Client
npx prisma format # format schema.prisma
npx prisma validate # schema validation
# Database workflows
npx prisma db pull # introspect existing DB into schema
npx prisma db push # apply schema to DB (no migration files)
# Migrations (preferred for teams/CI)
npx prisma migrate dev --name <msg> # create & apply a new migration (dev)
npx prisma migrate reset # drop DB & reapply migrations (danger)
npx prisma migrate deploy # apply checked-in migrations (CI/prod)
npx prisma migrate diff \ # generate SQL diff between two states
--from-url "$A" --to-url "$B" --script
npx prisma migrate resolve # mark a migration as applied/rolled back
# Tooling
npx prisma studio # open GUI to view/edit data
# Options (work with any command)
--schema ./path/to/schema.prisma
--skip-generate
--skip-seed
--preview-feature # (only if a doc asks for it)
15) Next.js patterns (App Router)
Single client instance
// src/lib/prisma.ts
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;
Route handler example
// app/api/posts/route.ts
import { prisma } from "@/src/lib/prisma";
export async function GET() {
const posts = await prisma.post.findMany({ take: 50, orderBy: { id: "desc" } });
return Response.json(posts);
}
Edge (optional)
import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";
export const runtime = "edge";
export async function GET() {
const prisma = new PrismaClient({ datasourceUrl: process.env.DATABASE_URL })
.$extends(withAccelerate());
const users = await prisma.user.findMany({ take: 10 });
return Response.json(users);
}
16) Patterns & recipes
Soft delete
model Post {
id Int @id @default(autoincrement())
deletedAt DateTime?
}
prisma.$use(async (params, next) => {
if (params.model === "Post" && ["delete", "deleteMany"].includes(params.action)) {
params.action = params.action === "delete" ? "update" : "updateMany";
// @ts-ignore add data for soft delete
params.args.data = { ...(params.args.data ?? {}), deletedAt: new Date() };
}
return next(params);
});
Cursor‑based pagination helper (client extension)
const prisma = new PrismaClient().$extends({
model: {
post: {
async page({ after, size = 20 }: { after?: number; size?: number }) {
return prisma.post.findMany({
take: size,
...(after ? { cursor: { id: after }, skip: 1 } : {}),
orderBy: { id: "asc" },
});
},
},
},
});
Zod integration for inputs
import { z } from "zod";
const CreatePost = z.object({ title: z.string().min(3), slug: z.string() });
export type CreatePost = z.infer<typeof CreatePost>;
17) Troubleshooting & common errors
-
P2002 (Unique constraint failed): handle with application error mapping; guard with
upsert
or pre‑checks -
P2021/P2022 (Table/column not found): run missing migrations or
db push
- Too many connections: reuse a singleton client; use poolers (PgBouncer) or Accelerate/edge client; avoid creating client per request
-
Shadow database errors: set
SHADOW_DATABASE_URL
; ensure user can create/drop schemas - Mongo connector differences: no joins; relations are manual via ids; many Prisma features still work with adjusted semantics
18) Mental models & best practices
- Treat
schema.prisma
as the source of truth for relational DBs in app‑owned schemas - Prefer
migrate
overdb push
for teams and CI/CD - Always check in migration folders; never edit a deployed migration
- Keep client usage close to your data layer; avoid passing Prisma Client deep into UI code
- Scope
select
/include
narrowly to reduce payloads and tighten types - For high‑scale read paths, consider an app‑level cache and/or read replica
Appendices
Type helpers you’ll actually use
import type { Prisma } from "@prisma/client";
// Derive the argument type of a query
type FindManyPostArgs = Prisma.PostFindManyArgs;
// Result type of a query
type PostPayload = Prisma.PostGetPayload<{ select: { id: true; title: true } }>;
Environment examples
# .env
DATABASE_URL="postgresql://user:pass@host:5432/db?schema=public&connection_limit=5&pool_timeout=30"
DIRECT_URL="postgresql://user:pass@writer-host:5432/db?schema=public" # for migrations
SHADOW_DATABASE_URL="postgresql://user:pass@host:5432/shadow"
MySQL ↔ Postgres gotchas
-
@default(autoincrement())
works on both; UUIDs are often better for sharding - Full‑text search: native on MySQL & Postgres (use
@@fulltext
) but syntax differs under the hood -
Json
type exists on both; prefer structured columns when you can
Mongo specifics
- Use
@db.ObjectId
on id fields; relations are manual (no joins) - Many relational helpers (
include
,select
, filters) still work but semantics differ
19) Quick cheat sheet
-
Modeling:
@id
,@default()
,@unique
,@relation(...)
,@@index([...])
-
Migrate:
migrate dev
,migrate deploy
,migrate reset
, avoid editing old migrations -
Query:
findMany
,findUnique
,create
,update
,delete
,upsert
,createMany
-
Filters:
{ contains, startsWith, in, notIn, lt, lte, gt, gte, equals, mode: "insensitive" }
-
Relations:
{ include: { rel: true } }
or{ select: { rel: { select: { ... } } } }
-
Pagination:
{ take, skip, cursor, orderBy }
-
Transactions:
prisma.$transaction([...])
orprisma.$transaction(async tx => { ... })
- Diagnostics: Prisma Studio, logs, OpenTelemetry tracing
🚀 Wrapping Up
Prisma ORM is a game-changer for modern web development, enabling developers to work seamlessly with databases while enjoying type safety and an excellent developer experience.
Whether you’re building with Next.js, scaling applications with Node.js, writing strongly-typed code in TypeScript, or integrating Prisma with React on the frontend, you’ll find Prisma fits naturally into your workflow.
Prisma also supports multiple databases like PostgreSQL, MySQL, MongoDB, and SQLite, making it the perfect companion for projects of all sizes.
By combining Prisma with tools like Supabase for serverless hosting, or using it in production with Postgres, you unlock massive flexibility and reliability for modern full-stack apps.
📚 Recommended Resources
- Official Prisma Docs → https://www.prisma.io/docs
- Prisma GitHub Repository → https://github.com/prisma/prisma
- Next.js Official Docs → https://nextjs.org/docs
- TypeScript Handbook → https://www.typescriptlang.org/docs
- Node.js Docs → https://nodejs.org/en/docs
- React Official Docs → https://react.dev
- PostgreSQL Docs → https://www.postgresql.org/docs
- MongoDB Docs → https://www.mongodb.com/docs
Additional SEO Resources
- Learn how to integrate Prisma ORM with Next.js, Node.js, and TypeScript for scalable applications.
- Best practices for using Prisma with PostgreSQL, MySQL, and MongoDB.
- How Prisma improves developer experience when building React and Next.js full-stack apps.
- A step-by-step guide to connect Prisma ORM with modern databases like Postgres and MongoDB.
- Using Prisma with Supabase, Vercel, and serverless environments.
Top comments (0)