From Basics to CRUD and Beyond
- What is Prisma?
- Core Concepts & Theory
- Prerequisites
- Project Setup
- Prisma Initialization
- Defining the Schema
- 6.1 Models & Fields
- 6.2 Field Types
- 6.3 Attributes & Modifiers
- 6.4 Relations
- 6.5 Full Schema Example
- Migrations
- 7.1 What is a Migration?
- 7.2 Running Migrations
- 7.3 Migration Workflow
- Generating Prisma Client
- CRUD Operations
- Advanced Querying
- 10.1 Filtering
- 10.2 Sorting & Pagination
- 10.3 Select & Include (Relations)
- 10.4 Aggregations
- 10.5 Transactions
- Seeding the Database
- Prisma Studio
- Error Handling
- Best Practices
- Quick Reference Cheat Sheet
Prisma is a next-generation, open-source ORM (Object-Relational Mapper) for Node.js and TypeScript. Unlike traditional ORMs, Prisma gives you:
- Auto-generated, fully type-safe client β no more guessing query return types
- A declarative schema β define your data models in a simple
.prismafile - First-class migrations β version-control your database schema changes
- An intuitive query API β reads like plain English, no raw SQL needed for most tasks
Prisma supports PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, and CockroachDB.
Why Prisma over other ORMs? Traditional ORMs like TypeORM or Sequelize use class-based models and decorators, which can lead to runtime type errors. Prisma generates its client from your schema at build time, so TypeScript knows the exact shape of every query result β catching bugs before they hit production.
| Approach | Example | Type Safety | Abstraction |
|---|---|---|---|
| Raw SQL | pg, mysql2 |
β None | Low |
| Query Builder | knex |
Medium | |
| Traditional ORM | TypeORM, Sequelize |
High | |
| Prisma | @prisma/client |
β Full | High |
Prisma is composed of three main tools:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your Application β
β (Node.js + TypeScript) β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
β uses
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
β Prisma Client (@prisma/client) β
β Auto-generated, type-safe database query client β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
β communicates via
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
β Prisma Query Engine β
β (Rust binary β translates Prisma queries to SQL) β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
β talks to
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
β Database β
β PostgreSQL / MySQL / SQLite / etc. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The three Prisma tools:
- Prisma Client β Auto-generated query client used in application code
- Prisma Migrate β Declarative migration system to manage schema evolution
- Prisma Studio β Web-based GUI to view and edit data in your database
The Prisma Schema (schema.prisma) is the single source of truth for:
- Your database connection (
datasourceblock) - The Prisma Client generator (
generatorblock) - Your data models (
modelblocks)
Everything flows from this one file β Prisma generates your migrations AND your TypeScript types from it.
When you run prisma generate, Prisma reads schema.prisma and emits a fully-typed client into node_modules/@prisma/client. This means:
// TypeScript KNOWS this is: { id: number; name: string; email: string }
const user = await prisma.user.findUnique({ where: { id: 1 } })
// β
This is fine
console.log(user?.name)
// β TypeScript error β 'phone' doesn't exist on User
console.log(user?.phone)No manual interface definitions needed. Your schema is your type definition.
Before starting, ensure you have:
- Node.js v18+ installed (
node --version) - npm or pnpm or yarn
- A running database (we'll use SQLite for simplicity, then show PostgreSQL)
- Basic TypeScript knowledge
# Create and enter your project directory
mkdir prisma-tutorial
cd prisma-tutorial
# Initialize a new npm project
npm init -y# Runtime dependency
npm install @prisma/client
# Development dependencies
npm install -D prisma typescript ts-node @types/node nodemon| Package | Purpose |
|---|---|
@prisma/client |
The auto-generated Prisma query client |
prisma |
Prisma CLI (migrations, generate, studio) |
typescript |
TypeScript compiler |
ts-node |
Run TypeScript files directly |
@types/node |
Node.js type definitions |
nodemon |
Auto-restart on file changes |
Create tsconfig.json in the project root:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Add scripts to package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "nodemon --exec ts-node src/index.ts",
"db:migrate": "prisma migrate dev",
"db:generate": "prisma generate",
"db:studio": "prisma studio",
"db:seed": "ts-node prisma/seed.ts"
}
}Create the source directory:
mkdir srcnpx prisma initThis command creates:
prisma/
schema.prisma β Your schema definition file
.env β Database connection URL (auto-created)
Option A β SQLite (easiest, no server needed):
Edit prisma/schema.prisma:
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}And remove the DATABASE_URL from .env (not needed for SQLite file path).
Option B β PostgreSQL:
.env file:
DATABASE_URL="postgresql://username:password@localhost:5432/mydb?schema=public"prisma/schema.prisma:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}Option C β MySQL:
.env file:
DATABASE_URL="mysql://username:password@localhost:3306/mydb"prisma/schema.prisma:
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}Security: Always add
.envto.gitignoreβ never commit database credentials.
A model maps to a database table. Each field maps to a column.
model User {
id Int @id @default(autoincrement())
name String
email String @unique
}This defines a users table with three columns: id, name, and email.
| Prisma Type | Maps To (PostgreSQL) | Maps To (SQLite) |
|---|---|---|
String |
TEXT / VARCHAR |
TEXT |
Int |
INTEGER |
INTEGER |
BigInt |
BIGINT |
INTEGER |
Float |
DOUBLE PRECISION |
REAL |
Decimal |
DECIMAL |
DECIMAL |
Boolean |
BOOLEAN |
INTEGER (0/1) |
DateTime |
TIMESTAMP |
DATETIME |
Json |
JSONB |
TEXT |
Bytes |
BYTEA |
BLOB |
model Example {
// Field attributes (single-field)
id Int @id // Primary key
uuid String @default(uuid()) // Auto UUID
createdAt DateTime @default(now()) // Auto timestamp
updatedAt DateTime @updatedAt // Auto-updated timestamp
email String @unique // Unique constraint
name String? // Optional field (nullable)
role Role @default(USER) // Enum with default
score Float @default(0.0) // Numeric default
data Json? // Nullable JSON
// Block attributes (multi-field)
@@unique([email, name]) // Composite unique
@@index([createdAt]) // Index for fast queries
@@map("examples_table") // Custom table name
}
enum Role {
USER
ADMIN
MODERATOR
}Modifier suffixes:
| Modifier | Meaning | Example |
|---|---|---|
? |
Optional (nullable) | String? |
[] |
List / array relation | Post[] |
| (none) | Required | String |
One-to-Many (most common):
One User has many Posts.
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[] // "virtual" field β not in DB, enables dot-access
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}One-to-One:
One User has one Profile.
model User {
id Int @id @default(autoincrement())
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String
userId Int @unique
user User @relation(fields: [userId], references: [id])
}Many-to-Many (implicit β Prisma manages join table):
model Post {
id Int @id @default(autoincrement())
tags Tag[]
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}Many-to-Many (explicit β you manage join table):
model Post {
id Int @id @default(autoincrement())
categories CategoryOnPost[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts CategoryOnPost[]
}
model CategoryOnPost {
postId Int
categoryId Int
assignedAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id])
category Category @relation(fields: [categoryId], references: [id])
@@id([postId, categoryId])
}// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
enum Role {
USER
ADMIN
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
profile Profile?
@@index([email])
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
tags Tag[]
@@index([authorId])
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}A migration is a recorded change to your database schema. Prisma tracks these in the prisma/migrations/ folder. Each migration is a SQL file that transforms your database from one state to another.
prisma/
migrations/
20240101_init/
migration.sql β SQL that creates initial tables
20240115_add_profile/
migration.sql β SQL that adds the Profile table
schema.prisma
This makes your schema changes version-controlled and reproducible β every developer runs the same migrations.
# Create and apply a new migration (development)
npx prisma migrate dev --name init
# Apply pending migrations (production)
npx prisma migrate deploy
# Reset database (drops all data, re-runs migrations) β dev only!
npx prisma migrate reset
# Check migration status
npx prisma migrate statusEdit schema.prisma
β
βΌ
npx prisma migrate dev --name describe_change
β
βΌ
Prisma diffs old β new schema
β
βΌ
Generates SQL migration file
β
βΌ
Applies migration to dev DB
β
βΌ
Runs `prisma generate` automatically
Important:
migrate devis for development only. Usemigrate deployin CI/CD and production pipelines.
After any schema change, regenerate the client:
npx prisma generateNote:
prisma migrate devcallsprisma generateautomatically. You only need to rungeneratemanually when the schema changes without a migration (e.g., adding/removing generators or when pulling from an existing database).
The generated client lives at node_modules/@prisma/client and contains types for every model, field, and query.
Create src/client.ts β a singleton Prisma client to share across your app:
// src/client.ts
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient({
log: ["query", "info", "warn", "error"], // Log SQL queries in dev
})
export default prismaWhy singleton? PrismaClient manages a database connection pool. Instantiating it multiple times creates multiple pools and exhausts connections. Always reuse a single instance.
Create src/index.ts as the entry point:
// src/index.ts
import prisma from "./client"
async function main() {
// Your code here
}
main()
.catch(console.error)
.finally(async () => {
await prisma.$disconnect()
})create β Insert a single record:
import prisma from "./client"
// Create a single user
const user = await prisma.user.create({
data: {
name: "Alice Johnson",
email: "alice@example.com",
role: "ADMIN",
},
})
console.log(user)
// { id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'ADMIN', ... }create with nested relations β Create user + profile in one query:
const userWithProfile = await prisma.user.create({
data: {
name: "Bob Smith",
email: "bob@example.com",
profile: {
create: {
bio: "Software developer based in London.",
},
},
posts: {
create: [
{ title: "Hello World", content: "My first post!", published: true },
{ title: "Draft Post", published: false },
],
},
},
include: {
profile: true,
posts: true,
},
})createMany β Insert multiple records at once:
const result = await prisma.user.createMany({
data: [
{ name: "Carol", email: "carol@example.com" },
{ name: "Dave", email: "dave@example.com" },
{ name: "Eve", email: "eve@example.com" },
],
skipDuplicates: true, // Skip records that violate unique constraints
})
console.log(result) // { count: 3 }Note:
createManydoes NOT support nested relation creation. Use multiplecreatecalls or a transaction for that.
findUnique β Fetch by unique field (id, email, etc.):
// By primary key
const userById = await prisma.user.findUnique({
where: { id: 1 },
})
// By unique email
const userByEmail = await prisma.user.findUnique({
where: { email: "alice@example.com" },
})
// Returns null if not found β always check!
if (!userById) {
throw new Error("User not found")
}findFirst β Fetch the first match:
const latestPost = await prisma.post.findFirst({
where: { published: true },
orderBy: { createdAt: "desc" },
})findMany β Fetch multiple records:
// Get all users
const allUsers = await prisma.user.findMany()
// Get all published posts
const publishedPosts = await prisma.post.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
take: 10, // LIMIT 10
skip: 0, // OFFSET 0
})findMany with related data:
const usersWithPosts = await prisma.user.findMany({
include: {
posts: {
where: { published: true },
orderBy: { createdAt: "desc" },
},
profile: true,
},
})
// usersWithPosts[0].posts is fully typed as Post[]select β Return only specific fields:
const userNames = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
// password: true β safely exclude sensitive fields
},
})
// Returns: { id: number; name: string; email: string }[]
// NOT the full User type
includevsselect: Useincludeto add related models on top of all fields. Useselectto cherry-pick specific fields. You cannot use both at the top level simultaneously.
update β Update a single record (must match by unique field):
const updatedUser = await prisma.user.update({
where: { id: 1 },
data: {
name: "Alice Johnson-Smith",
role: "ADMIN",
},
})update with atomic operations:
// Increment, decrement, multiply, divide
await prisma.post.update({
where: { id: 5 },
data: {
viewCount: { increment: 1 }, // viewCount + 1
score: { multiply: 1.1 }, // score * 1.1
},
})
// For strings: set, push (for lists in PostgreSQL)
await prisma.user.update({
where: { id: 1 },
data: {
name: { set: "New Name" }, // same as just: name: "New Name"
},
})upsert β Update if exists, create if not:
const user = await prisma.user.upsert({
where: { email: "alice@example.com" },
update: {
name: "Alice Updated",
},
create: {
name: "Alice New",
email: "alice@example.com",
},
})updateMany β Update multiple records:
const result = await prisma.post.updateMany({
where: { authorId: 1 },
data: { published: true },
})
console.log(result) // { count: 5 } β number of records updatedNote:
updateManyreturns{ count: number }, not the updated records. UsefindManyafter if you need the updated data.
delete β Delete a single record:
const deletedUser = await prisma.user.delete({
where: { id: 1 },
})
// Returns the deleted recorddeleteMany β Delete multiple records:
// Delete all unpublished posts
const result = await prisma.post.deleteMany({
where: { published: false },
})
console.log(result) // { count: 3 }
// Delete ALL records (use with caution!)
await prisma.user.deleteMany({})Cascades: If you define
onDelete: Cascadein your schema, deleting a User will also delete their Posts and Profile. Without it, deleting a User with existing Posts will throw a foreign key constraint error.
Prisma's where clause supports rich filter operators:
const results = await prisma.user.findMany({
where: {
// Equality (implicit)
role: "ADMIN",
// String filters
name: {
contains: "Alice", // LIKE '%Alice%'
startsWith: "Al", // LIKE 'Al%'
endsWith: "ce", // LIKE '%ce'
mode: "insensitive", // Case-insensitive (PostgreSQL)
},
// Numeric filters
id: {
gt: 5, // > 5
gte: 5, // >= 5
lt: 10, // < 10
lte: 10, // <= 10
not: 7, // != 7
},
// Date filters
createdAt: {
gte: new Date("2024-01-01"),
lt: new Date("2025-01-01"),
},
// IN / NOT IN
id: { in: [1, 2, 3] },
role: { notIn: ["BANNED"] },
// Null checks
profile: { is: null }, // WHERE profile IS NULL
profile: { isNot: null }, // WHERE profile IS NOT NULL
}
})Combining filters with AND / OR / NOT:
const posts = await prisma.post.findMany({
where: {
AND: [
{ published: true },
{ createdAt: { gte: new Date("2024-01-01") } },
],
OR: [
{ title: { contains: "TypeScript" } },
{ title: { contains: "Prisma" } },
],
NOT: {
authorId: 99,
},
},
})Filtering on related models:
// Users who have at least one published post
const activeAuthors = await prisma.user.findMany({
where: {
posts: {
some: { published: true },
},
},
})
// Users with NO posts
const usersNoPosts = await prisma.user.findMany({
where: {
posts: { none: {} },
},
})
// Users where ALL posts are published
const fullyPublishedAuthors = await prisma.user.findMany({
where: {
posts: {
every: { published: true },
},
},
})Sorting:
// Sort by single field
const posts = await prisma.post.findMany({
orderBy: { createdAt: "desc" },
})
// Sort by multiple fields
const users = await prisma.user.findMany({
orderBy: [
{ role: "asc" },
{ name: "asc" },
],
})
// Sort by related field count
const popularAuthors = await prisma.user.findMany({
orderBy: {
posts: { _count: "desc" },
},
})Offset pagination (simple):
const PAGE_SIZE = 10
async function getPage(page: number) {
return prisma.post.findMany({
skip: (page - 1) * PAGE_SIZE,
take: PAGE_SIZE,
orderBy: { createdAt: "desc" },
})
}
const page1 = await getPage(1) // First 10
const page2 = await getPage(2) // Next 10Cursor-based pagination (efficient for large datasets):
// First page
const firstPage = await prisma.post.findMany({
take: 10,
orderBy: { id: "asc" },
})
// Next page β pass the last item's cursor
const lastId = firstPage[firstPage.length - 1].id
const nextPage = await prisma.post.findMany({
take: 10,
skip: 1, // Skip the cursor itself
cursor: { id: lastId },
orderBy: { id: "asc" },
})include β Eager load related models:
const post = await prisma.post.findUnique({
where: { id: 1 },
include: {
author: true, // Include full User
tags: true, // Include all Tags
},
})
// post.author is User, post.tags is Tag[]Nested includes:
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
include: {
tags: true, // Include tags inside each post
},
where: { published: true },
orderBy: { createdAt: "desc" },
take: 5,
},
profile: true,
},
})select β Return specific fields only:
const userSummary = await prisma.user.findMany({
select: {
id: true,
name: true,
_count: {
select: { posts: true }, // Include count of related posts
},
},
})
// userSummary[0]._count.posts β number// Count
const totalUsers = await prisma.user.count()
const adminCount = await prisma.user.count({
where: { role: "ADMIN" },
})
// Aggregate (min, max, sum, avg, count)
const stats = await prisma.post.aggregate({
_count: { id: true },
_avg: { viewCount: true },
_sum: { viewCount: true },
_min: { createdAt: true },
_max: { createdAt: true },
where: { published: true },
})
console.log(stats._avg.viewCount) // average views
// GroupBy
const postsByAuthor = await prisma.post.groupBy({
by: ["authorId"],
_count: { id: true },
_sum: { viewCount: true },
orderBy: { _count: { id: "desc" } },
})Sequential transactions (one fails β all roll back):
// Method 1: $transaction with array (parallel execution order not guaranteed)
const [newUser, newPost] = await prisma.$transaction([
prisma.user.create({ data: { name: "Frank", email: "frank@example.com" } }),
prisma.post.create({ data: { title: "Transaction Post", authorId: 1 } }),
])Interactive transactions (full control with rollback):
const result = await prisma.$transaction(async (tx) => {
// Use `tx` instead of `prisma` β all ops share one transaction
const user = await tx.user.create({
data: { name: "Grace", email: "grace@example.com" },
})
const post = await tx.post.create({
data: {
title: "Grace's First Post",
authorId: user.id,
},
})
// If anything throws here, the entire transaction rolls back
if (!user || !post) {
throw new Error("Creation failed")
}
return { user, post }
})Use transactions when: you need multiple writes to succeed or fail together β e.g., transferring funds between accounts, creating a user + their default settings simultaneously.
Create prisma/seed.ts:
// prisma/seed.ts
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
async function main() {
console.log("π± Seeding database...")
// Clean up existing data (optional)
await prisma.post.deleteMany({})
await prisma.user.deleteMany({})
// Create users with nested posts
const alice = await prisma.user.create({
data: {
name: "Alice Johnson",
email: "alice@example.com",
role: "ADMIN",
profile: {
create: { bio: "Full-stack developer and coffee enthusiast." },
},
posts: {
create: [
{
title: "Getting Started with Prisma",
content: "Prisma makes database access easy and type-safe...",
published: true,
},
{
title: "Advanced TypeScript Patterns",
content: "Let's explore some advanced TypeScript techniques...",
published: true,
},
],
},
},
})
const bob = await prisma.user.create({
data: {
name: "Bob Smith",
email: "bob@example.com",
posts: {
create: [
{ title: "My Draft Post", published: false },
],
},
},
})
console.log("β
Seeded:", { alice: alice.name, bob: bob.name })
}
main()
.catch((e) => {
console.error("β Seeding failed:", e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})Register the seed script in package.json:
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}Run the seed:
npx prisma db seed
# or with npm script:
npm run db:seedSeeding also runs automatically after prisma migrate reset.
Prisma Studio is a visual database browser. Launch it with:
npx prisma studioThis opens a web UI at http://localhost:5555 where you can:
- Browse all tables and records
- Filter, sort, and paginate data
- Create, edit, and delete records manually
- Explore relations between models
Great for debugging during development β no SQL client needed.
Prisma throws specific error types you can catch and handle:
import { PrismaClient, Prisma } from "@prisma/client"
const prisma = new PrismaClient()
async function createUser(email: string, name: string) {
try {
const user = await prisma.user.create({
data: { email, name },
})
return user
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// P2002 β Unique constraint violation
if (error.code === "P2002") {
const field = error.meta?.target as string[]
throw new Error(`A user with this ${field?.join(", ")} already exists.`)
}
// P2025 β Record not found
if (error.code === "P2025") {
throw new Error("Record not found.")
}
// P2003 β Foreign key constraint failed
if (error.code === "P2003") {
throw new Error("Related record does not exist.")
}
console.error("Prisma error code:", error.code)
}
if (error instanceof Prisma.PrismaClientValidationError) {
throw new Error("Invalid data provided to Prisma.")
}
throw error // Re-throw unknown errors
}
}Common Prisma error codes:
| Code | Meaning |
|---|---|
P2002 |
Unique constraint violation |
P2003 |
Foreign key constraint failed |
P2025 |
Record not found |
P2014 |
Relation violation |
P2000 |
Value too long for column |
P1001 |
Can't reach database server |
P1002 |
Database server timeout |
1. Singleton Prisma Client
Always export and reuse a single PrismaClient instance. In Next.js (which hot-reloads in dev), use this pattern:
// src/lib/prisma.ts
import { PrismaClient } from "@prisma/client"
declare global {
var __prisma: PrismaClient | undefined
}
const prisma = globalThis.__prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== "production") {
globalThis.__prisma = prisma
}
export default prisma2. Always disconnect in scripts
async function main() { /* ... */ }
main()
.catch(console.error)
.finally(() => prisma.$disconnect())3. Use select for sensitive data
Never return the full model when only a subset is needed:
// β
Safe β never accidentally expose password hash
const user = await prisma.user.findUnique({
where: { id },
select: { id: true, name: true, email: true },
})4. Validate before writing
Use Zod or similar to validate input before passing to Prisma:
import { z } from "zod"
const CreateUserSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
})
const validated = CreateUserSchema.parse(requestBody)
const user = await prisma.user.create({ data: validated })5. Use transactions for multi-step writes
Any time two or more writes must succeed together, use $transaction.
6. Add indexes for frequently queried fields
model Post {
id Int @id @default(autoincrement())
authorId Int
// β Without this, queries like findMany({ where: { authorId } }) do a full scan
@@index([authorId])
}7. Enable query logging in development
const prisma = new PrismaClient({
log: process.env.NODE_ENV === "development"
? ["query", "warn", "error"]
: ["warn", "error"],
})// ββ SETUP ββββββββββββββββββββββββββββββββββββββββββββββ
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
// ββ CREATE βββββββββββββββββββββββββββββββββββββββββββββ
await prisma.user.create({ data: { name, email } })
await prisma.user.createMany({ data: [...], skipDuplicates: true })
// ββ READ βββββββββββββββββββββββββββββββββββββββββββββββ
await prisma.user.findUnique({ where: { id } })
await prisma.user.findFirst({ where: { role: "ADMIN" } })
await prisma.user.findMany({ where: {...}, orderBy: {...}, take, skip })
// ββ UPDATE βββββββββββββββββββββββββββββββββββββββββββββ
await prisma.user.update({ where: { id }, data: { name } })
await prisma.user.upsert({ where: { email }, update: {...}, create: {...} })
await prisma.user.updateMany({ where: {...}, data: {...} })
// ββ DELETE βββββββββββββββββββββββββββββββββββββββββββββ
await prisma.user.delete({ where: { id } })
await prisma.user.deleteMany({ where: {...} })
// ββ RELATIONS ββββββββββββββββββββββββββββββββββββββββββ
// Eager load
await prisma.user.findUnique({ where: { id }, include: { posts: true } })
// Nested create
await prisma.user.create({ data: { ..., posts: { create: [...] } } })
// ββ FILTERS ββββββββββββββββββββββββββββββββββββββββββββ
where: { name: { contains: "Alice", mode: "insensitive" } }
where: { id: { gt: 5, lte: 20 } }
where: { AND: [...], OR: [...], NOT: {...} }
where: { posts: { some: { published: true } } }
// ββ PAGINATION βββββββββββββββββββββββββββββββββββββββββ
{ take: 10, skip: 20 } // Offset
{ take: 10, cursor: { id: lastId }, skip: 1 } // Cursor
// ββ AGGREGATION ββββββββββββββββββββββββββββββββββββββββ
await prisma.post.count({ where: { published: true } })
await prisma.post.aggregate({ _avg: { views: true } })
await prisma.post.groupBy({ by: ["authorId"], _count: true })
// ββ TRANSACTIONS βββββββββββββββββββββββββββββββββββββββ
await prisma.$transaction([op1, op2, op3])
await prisma.$transaction(async (tx) => { /* use tx.model... */ })
// ββ CLI ββββββββββββββββββββββββββββββββββββββββββββββββ
// npx prisma init
// npx prisma migrate dev --name <name>
// npx prisma migrate deploy
// npx prisma generate
// npx prisma db seed
// npx prisma studio
// npx prisma migrate resetGenerated with β€οΈ β Prisma v5.x | Node.js v18+ | TypeScript v5.x