Backend DevelopmentDatabasesORMs
Learn Prisma
A comprehensive Guide to learn Prisma.
Welcome to the comprehensive Prisma course. Prisma is a next-generation ORM for Node.js and TypeScript.
Table of contents
-
Getting Started
-
Chapter I
-
Chapter II
-
Chapter III
-
Chapter IV
-
Appendix
What is Prisma?
Prisma is a next-generation ORM that consists of three main tools:
- Prisma Client: Type-safe database client
- Prisma Migrate: Database migration tool
- Prisma Studio: GUI for viewing and editing data
Key Characteristics
- Type-Safe: Full TypeScript support
- Auto-Completion: Excellent IDE integration
- Clean API: Intuitive query API
- Migrations: Version controlled schema
- Multi-Database: PostgreSQL, MySQL, SQLite, SQL Server, MongoDB
Why Prisma?
1. Type Safety
Prisma generates fully typed clients from your schema.
2. Developer Experience
Excellent auto-completion and error messages.
3. Modern Architecture
Built for modern JavaScript runtimes.
4. Migration System
Easy database schema evolution.
5. Performance
Optimized queries with connection pooling.
Installation and Setup
Installation
npm install prisma --save-dev
npm install @prisma/clientInitialize
npx prisma initProject Structure
my-project/
├── prisma/
│ └── schema.prisma
├── src/
│ └── index.ts
└── package.jsonBasic Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}Environment
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"Schema
Data Source
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// Optional: Shadow database for migrations
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}Generators
generator client {
provider = "prisma-client-js"
// Output path for generated client
output = "../node_modules/.prisma/client"
// Preview features
previewFeatures = ["fullTextSearch", "relationalActions"]
}Supported Databases
// PostgreSQL
provider = "postgresql"
// MySQL
provider = "mysql"
// SQLite
provider = "sqlite"
// SQL Server
provider = "sqlserver"
// MongoDB
provider = "mongodb"Models
Basic Model
model User {
id Int @id @default(autoincrement())
name String
email String @unique
age Int?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Field Types
model Post {
id Int @id
title String // String
views Int // Integer
rating Float // Float
published Boolean // Boolean
content String? // Optional
createdAt DateTime // DateTime
metadata Json? // JSON
price Decimal @db.Decimal(10, 2) // Decimal
slug String @db.VarChar(50) // Custom type
}Default Values
model User {
id Int @id @default(autoincrement())
name String @default("Anonymous")
active Boolean @default(true)
createdAt DateTime @default(now())
role Role @default(USER)
uuid String @default(uuid())
slug String @default(cuid())
}
enum Role {
USER
ADMIN
}Optional Fields
model User {
id Int @id
name String
email String? // Optional String
age Int? // Optional Int
}Unique Fields
model User {
id Int @id
email String @unique
username String @unique
// Composite unique
@@unique([firstName, lastName])
}Indexes
model User {
id Int @id
email String
createdAt DateTime
@@index([email])
@@index([createdAt, email])
}Map to Existing Database
model User {
id Int @id @map("user_id")
name String @map("user_name")
@@map("users") // Table name
}Relations
One-to-One
model User {
id Int @id
profile Profile?
}
model Profile {
id Int @id
bio String
userId Int @unique // One-to-one
user User @relation(fields: [userId], references: [id])
}One-to-Many
model User {
id Int @id
orders Order[]
}
model Order {
id Int @id
userId Int
user User @relation(fields: [userId], references: [id])
}Many-to-Many
model Post {
id Int @id
title String
tags Tag[]
}
model Tag {
id Int @id
name String
posts Post[]
}
// Or explicit join table
model PostTag {
postId Int
tagId Int
post Post @relation(fields: [postId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@id([postId, tagId])
}Self Relations
model Employee {
id Int @id
name String
managerId Int?
manager Employee? @relation("Manager", fields: [managerId], references: [id])
reports Employee[] @relation("Manager")
}Relation Fields
model Post {
id Int @id
authorId Int
author User @relation(fields: [authorId], references: [id])
// Relation mode
// relationMode = "prisma" // Default
}
// Or with relationMode
model Post {
id Int @id
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
@@index([authorId])
}On Delete/Update Actions
model Post {
id Int @id
authorId Int
author User @relation(fields: [authorId], references: [id],
onDelete: Cascade, // Cascade, Restrict, SetNull, NoAction, SetDefault
onUpdate: Cascade)
}Migrations
Creating Migrations
# Create migration from schema changes
npx prisma migrate dev --name add_users_table
# Create empty migration
npx prisma migrate dev --name init
# Reset all migrations
npx prisma migrate resetApply Migrations
# Apply pending migrations
npx prisma migrate deploy
# Dev workflow
npx prisma migrate dev
# Production
npx prisma migrate deployMigration History
# Check status
npx prisma migrate status
# Resolve migration issues
npx prisma migrate resolve --applied 20220101000000_init
npx prisma migrate resolve --rolled-back 20220101000000_initDevelopment Workflow
# 1. Update schema.prisma
# 2. Run migration
npx prisma migrate dev
# 3. Generate client
npx prisma generateClient Setup
Basic Setup
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
const users = await prisma.user.findMany();
console.log(users);
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());Singleton Pattern
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}With Connection Pooling
// For serverless
import { PrismaClient } from "@prisma/client";
import { Pool } from "generic-pool";
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});CRUD Operations
Create
// Create single record
const user = await prisma.user.create({
data: {
name: "John",
email: "john@example.com",
age: 30,
},
});
// Create with nested relations
const post = await prisma.post.create({
data: {
title: "Hello World",
author: {
create: {
name: "John",
email: "john@example.com",
},
},
},
});
// Create multiple
const users = await prisma.user.createMany({
data: [
{ name: "John", email: "john1@example.com" },
{ name: "Jane", email: "john2@example.com" },
],
});
// Create and return specific fields
const user = await prisma.user.create({
data: { name: "John", email: "john@example.com" },
select: { id: true, name: true },
});Read
// Find unique
const user = await prisma.user.findUnique({
where: { id: 1 },
});
const user = await prisma.user.findUnique({
where: { email: "john@example.com" },
});
// Find first
const user = await prisma.user.findFirst({
where: { name: { startsWith: "J" } },
});
// Find many
const users = await prisma.user.findMany();
// Count
const count = await prisma.user.count();Update
// Update single
const user = await prisma.user.update({
where: { id: 1 },
data: { name: "Updated Name" },
});
// Update many
const count = await prisma.user.updateMany({
where: { active: false },
data: { active: true },
});
// Update with nested relations
const post = await prisma.post.update({
where: { id: 1 },
data: {
title: "Updated Title",
author: {
update: { name: "New Author Name" },
},
},
});
// Upsert
const user = await prisma.user.upsert({
where: { email: "john@example.com" },
update: { name: "Updated Name" },
create: { name: "John", email: "john@example.com" },
});Delete
// Delete single
const user = await prisma.user.delete({
where: { id: 1 },
});
// Delete many
const count = await prisma.user.deleteMany({
where: { createdAt: { lt: new Date("2020-01-01") } },
});
// Delete all
await prisma.user.deleteMany();Querying
Select Fields
// Select specific fields
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
},
});
// Include relations
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true,
profile: true,
},
});
// Nested include
const post = await prisma.post.findUnique({
where: { id: 1 },
include: {
author: {
include: { profile: true },
},
tags: true,
},
});
// Include with filter
const users = await prisma.user.findMany({
include: {
posts: {
where: { published: true },
},
},
});Order By
// Order by single field
const users = await prisma.user.findMany({
orderBy: { name: "asc" },
});
// Order by multiple fields
const posts = await prisma.post.findMany({
orderBy: [{ createdAt: "desc" }, { title: "asc" }],
});
// Order by relation aggregate
const users = await prisma.user.findMany({
orderBy: {
posts: { _count: "desc" },
},
});Distinct
const users = await prisma.user.findMany({
distinct: ["country"],
select: { country: true },
});Filtering
Basic Where
// Simple equality
await prisma.user.findMany({ where: { id: 1 } });
await prisma.user.findMany({ where: { active: true } });
// Multiple conditions
await prisma.user.findMany({
where: {
id: { gt: 10 },
active: true,
},
});Comparison Operators
// Number comparisons
{ id: { gt: 10 } } // Greater than
{ id: { gte: 10 } } // Greater than or equal
{ id: { lt: 10 } } // Less than
{ id: { lte: 10 } } // Less than or equal
{ id: { in: [1, 2, 3] } } // In list
{ id: { notIn: [1, 2, 3] } } // Not in list
// String comparisons
{ name: { contains: 'John' } }
{ name: { startsWith: 'J' } }
{ name: { endsWith: 'n' } }
{ name: { equals: 'John' } }
{ name: { mode: 'insensitive' } } // Case insensitive
// Boolean
{ active: { not: false } }
{ deletedAt: { not: null } }
{ deletedAt: { equals: null } }Logical Operators
// AND
await prisma.user.findMany({
where: {
AND: [{ active: true }, { age: { gte: 18 } }],
},
});
// OR
await prisma.user.findMany({
where: {
OR: [{ role: "ADMIN" }, { email: { endsWith: "@company.com" } }],
},
});
// NOT
await prisma.user.findMany({
where: {
NOT: { active: false },
},
});
// Simplified
await prisma.user.findMany({
where: {
active: true,
age: { gte: 18 },
},
});NULL Checks
{
profile: {
is: null;
}
} // Is NULL
{
profile: {
isNot: null;
}
} // Is NOT NULLFull-text Search (PostgreSQL)
// schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["fullTextSearch"]
}
model Post {
id Int @id
title String
content String @db.Text
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}const posts = await prisma.post.findMany({
where: {
OR: [
{ title: { search: "hello & world" } },
{ content: { search: "hello & world" } },
],
},
});Pagination
Offset Pagination
// Skip/take
const users = await prisma.user.findMany({
skip: 0,
take: 10,
});
// Page based
const page = 2;
const pageSize = 10;
const users = await prisma.user.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
});Cursor Pagination
// Using cursor (more efficient for large datasets)
const users = await prisma.user.findMany({
take: 10,
cursor: { id: lastId },
skip: 1,
});
// With ordering
const users = await prisma.user.findMany({
take: 10,
orderBy: { createdAt: "desc" },
cursor: { id: lastId },
skip: 1,
});Total Count
const [users, total] = await Promise.all([
prisma.user.findMany({ take: 10 }),
prisma.user.count(),
]);Aggregation
Basic Aggregation
// Count
const count = await prisma.user.count();
// Count with filter
const count = await prisma.user.count({
where: { active: true },
});
// Select aggregate
const result = await prisma.user.aggregate({
_count: { id: true },
_max: { age: true },
_min: { age: true },
_avg: { age: true },
_sum: { age: true },
});Group By
// Group by single field
const result = await prisma.user.groupBy({
by: ["country"],
_count: { id: true },
});
// Group by with filter
const result = await prisma.user.groupBy({
by: ["status"],
where: { active: true },
_count: { id: true },
});
// Group by with having
const result = await prisma.user.groupBy({
by: ["country"],
_count: { id: true },
having: { id: { _count: { gt: 5 } } },
});Advanced Aggregation
const result = await prisma.order.groupBy({
by: ["userId", "status"],
_sum: { total: true },
_count: { id: true },
where: { status: "completed" },
});Transactions
Interactive Transactions
async function transfer(fromId: number, toId: number, amount: number) {
return prisma.$transaction(async (tx) => {
const from = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } },
});
if (from.balance < 0) {
throw new Error("Insufficient funds");
}
const to = await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } },
});
return { from, to };
});
}Sequential Transactions
// Sequential operations
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { name: "John" } }),
prisma.post.create({ data: { title: "Hello" } }),
]);Isolation Levels
await prisma.$transaction(
async (tx) => {
// Transaction with isolation level
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
},
);Raw Queries
Raw Queries Limitations
// Prisma doesn't support raw SQL for all operations
// Use for simple queries only
const result = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${1}`;$queryRaw
// Template tag
const users = await prisma.$queryRaw`SELECT * FROM users`;
// With parameters (safe)
const user = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${1}`;
// Using Prisma.sql
import { Prisma } from "@prisma/client";
const users = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM users WHERE id = ${1}`,
);$executeRaw
// For INSERT, UPDATE, DELETE
const result = await prisma.$executeRaw`DELETE FROM users WHERE active = false`;
// With parameters
await prisma.$executeRaw`UPDATE users SET active = false WHERE created_at < ${lastYear}`;Raw Query Results
// Returns array of objects
const result = await prisma.$queryRaw<{ id: number; name: string }>`
SELECT id, name FROM users
`;TypeScript Integration
Generated Types
import { PrismaClient, Prisma } from "@prisma/client";
// User type from schema
const user: Prisma.UserGetPayload<{}> = {};
// Or use Select/Include types
const user = await prisma.user.findUnique({
where: { id: 1 },
select: { id: true, name: true },
});
// user is { id: number; name: string }Custom Types
// Define custom input type
type CreateUserInput = Prisma.UserCreateInput;
const createUser = (data: CreateUserInput) => {
return prisma.user.create({ data });
};Extending Client
// prisma.ts
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient().$extends({
model: {
user: {
async findByEmail(email: string) {
return this.findUnique({ where: { email } });
},
},
},
});
// Usage
const user = await prisma.user.findByEmail("john@example.com");Best Practices
Project Structure
src/
├── lib/
│ └── prisma.ts # Prisma client singleton
├── services/
│ └── user.service.ts # Business logic
├── controllers/
│ └── user.controller.ts
└── routes/
└── user.routes.tsError Handling
import { Prisma } from "@prisma/client";
try {
const user = await prisma.user.create({
data: { email: "existing@example.com" },
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
console.log("Email already exists");
}
}
}Error Codes
// P2002: Unique constraint failed
// P2003: Foreign key constraint failed
// P2025: Record not found
// P2011: Null constraint violation
// P2000: Value too long for columnUse Select/Include Wisely
// Don't fetch everything when you only need a few fields
const user = await prisma.user.findUnique({
where: { id: 1 },
select: { id: true, name: true },
});
// Always include relations you need
const post = await prisma.post.findUnique({
where: { id: 1 },
include: { author: true, tags: true },
});Performance
Query Optimization
// Use select instead of include when possible
const users = await prisma.user.findMany({
select: { id: true, name: true },
where: { active: true },
});
// Use cursor for pagination
const users = await prisma.user.findMany({
take: 10,
cursor: { id: lastId },
skip: 1,
});Avoid N+1
// Bad: N+1 query
const users = await prisma.user.findMany();
for (const user of users) {
const postCount = await prisma.post.count({ where: { authorId: user.id } });
}
// Good: Single query with include
const users = await prisma.user.findMany({
include: {
_count: { select: { posts: true } },
},
});Connection Pooling
// For serverless (Vercel, AWS Lambda)
// Use connection pooler like PgBouncer or Prisma Data Proxy
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});Batch Operations
// Use createMany instead of multiple creates
await prisma.user.createMany({
data: users.map((u) => ({ name: u.name, email: u.email })),
});
// Use updateMany instead of multiple updates
await prisma.user.updateMany({
where: { active: false },
data: { status: "inactive" },
});Testing
Test Setup
import { PrismaClient } from "@prisma/client";
import { beforeEach, afterEach, describe, it, expect } from "vitest";
describe("User Service", () => {
let prisma: PrismaClient;
beforeEach(async () => {
// Use separate test database
prisma = new PrismaClient({
datasources: {
db: { url: "postgresql://test:test@localhost:5432/test" },
},
});
await prisma.$connect();
});
afterEach(async () => {
// Clean up
await prisma.user.deleteMany();
await prisma.$disconnect();
});
it("should create a user", async () => {
const user = await prisma.user.create({
data: { name: "John", email: "john@example.com" },
});
expect(user.id).toBeDefined();
expect(user.name).toBe("John");
});
});Unit Testing with Mocks
import { PrismaClient } from "@prisma/client";
const mockPrisma = {
user: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn().mockResolvedValue({ id: 1, name: "John" }),
delete: vi.fn().mockResolvedValue({ id: 1 }),
},
} as unknown as PrismaClient;
// Use mock in testsNext Steps
Now that you know Prisma fundamentals:
- Explore Prisma Pulse for real-time subscriptions
- Learn about Prisma Data Platform
- Study advanced query optimization
- Learn about multi-database patterns
- Explore Prisma with Next.js, NestJS, or Remix