Prisma ORM — Complete TypeScript Guide 2026
Advertisement
Introduction
Why This Matters
Prisma is the most widely adopted TypeScript ORM in 2026, used by companies ranging from startups to enterprises. Its auto-generated client, schema-first approach, and excellent developer experience make it the fastest way to start building a type-safe database layer. If you are building a full-stack TypeScript application with Next.js, NestJS, or Express, Prisma is likely the smoothest path to a production-ready data layer.
Installation and Initialization
npm install @prisma/client
npm install --save-dev prisma
# Initialize Prisma in your project
npx prisma initThis creates prisma/schema.prisma and a .env file with a DATABASE_URL placeholder.
Defining the Schema
Prisma uses its own schema language — the Prisma Schema Language (PSL). All models, relations, and database settings live in prisma/schema.prisma.
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
enum Role {
USER
ADMIN
MODERATOR
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
role Role @default(USER)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
profile Profile?
@@index([role])
}
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
body String
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags Tag[]
createdAt DateTime @default(now())
@@index([authorId])
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}After editing the schema, generate or update the Prisma Client:
npx prisma generatePrisma Client — CRUD Operations
// src/db/client.ts
import { PrismaClient } from '@prisma/client';
// Single instance pattern — prevents connection pool exhaustion in dev
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'warn', 'error']
: ['warn', 'error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}// src/db/users.ts
import { prisma } from './client';
import { Prisma, User } from '@prisma/client';
// Find by ID with related data
export async function getUserWithPosts(id: number) {
return prisma.user.findUnique({
where: { id },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10,
},
profile: true,
},
});
}
// Create with nested write
export async function createUserWithProfile(
email: string,
name: string,
bio: string
) {
return prisma.user.create({
data: {
email,
name,
profile: {
create: { bio },
},
},
include: { profile: true },
});
}
// Filtered list with pagination
export async function listUsers(
page: number,
pageSize: number,
search?: string
): Promise<{ users: User[]; total: number }> {
const where: Prisma.UserWhereInput = search
? { OR: [{ email: { contains: search } }, { name: { contains: search } }] }
: {};
const [users, total] = await prisma.$transaction([
prisma.user.findMany({
where,
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: 'desc' },
}),
prisma.user.count({ where }),
]);
return { users, total };
}
// Update
export async function updateUserRole(id: number, role: 'USER' | 'ADMIN') {
return prisma.user.update({
where: { id },
data: { role },
});
}
// Upsert — create or update
export async function upsertUser(email: string, name: string) {
return prisma.user.upsert({
where: { email },
create: { email, name },
update: { name },
});
}
// Soft delete pattern
export async function deactivateUser(id: number) {
return prisma.user.update({
where: { id },
data: { isActive: false },
});
}Transactions
Prisma supports two types of transactions: sequential (in $transaction([])) and interactive (with a callback).
// Sequential transaction — runs queries in order, all succeed or all fail
export async function transferPosts(fromUserId: number, toUserId: number) {
await prisma.$transaction([
prisma.post.updateMany({
where: { authorId: fromUserId },
data: { authorId: toUserId },
}),
prisma.user.update({
where: { id: fromUserId },
data: { isActive: false },
}),
]);
}
// Interactive transaction — full control, can read within the transaction
export async function createPostWithTagsTransaction(
authorId: number,
title: string,
body: string,
tagNames: string[]
) {
return prisma.$transaction(async (tx) => {
const post = await tx.post.create({
data: { title, body, authorId },
});
// Upsert tags and connect them
const tags = await Promise.all(
tagNames.map((name) =>
tx.tag.upsert({
where: { name },
create: { name },
update: {},
})
)
);
await tx.post.update({
where: { id: post.id },
data: {
tags: { connect: tags.map((t) => ({ id: t.id })) },
},
});
return tx.post.findUnique({
where: { id: post.id },
include: { tags: true },
});
});
}Migrations
# Create and apply a migration
npx prisma migrate dev --name add_user_bio
# Apply migrations in production (no dev mode)
npx prisma migrate deploy
# Check migration status
npx prisma migrate status
# Open Prisma Studio (database browser)
npx prisma studioRaw Queries
Sometimes you need SQL that Prisma cannot express. Use $queryRaw for typed results.
import { Prisma } from '@prisma/client';
interface UserStats {
id: number;
email: string;
post_count: bigint;
}
export async function getUserStats(): Promise<UserStats[]> {
return prisma.$queryRaw<UserStats[]>(
Prisma.sql`
SELECT u.id, u.email, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
GROUP BY u.id, u.email
ORDER BY post_count DESC
LIMIT 10
`
);
}Common Mistakes
Mistake 1 — Creating a new PrismaClient on every request in Next.js: Next.js hot-reloads create new module instances, exhausting database connections. Use the global singleton pattern shown above.
Mistake 2 — Over-using include: deeply nested include can generate massive JOIN queries. Use select to fetch only the columns you need.
Mistake 3 — Ignoring $transaction for multi-step writes: if two prisma.create() calls run independently and the second fails, the first is not rolled back — you get partial data.
Mistake 4 — Using $queryRaw with string interpolation: prisma.$queryRaw(SELECT * FROM users WHERE id = ${id}) is SQL injection. Always use Prisma.sql tagged template literals.
Best Practices
- Use
selectinstead ofincludewhen you only need specific fields — it reduces query payload and improves performance. - Enable
log: ['query']in development to see the SQL Prisma generates and spot N+1 problems early. - Use
prisma.$transactionfor any operation spanning multiple models — it is the only way to guarantee consistency. - Use
Prisma.UserGetPayload<...>utility types to type function return values without manually duplicating types. - Run
prisma migrate deploy(notdev) in CI/CD pipelines to apply migrations without resetting the database.
Key Takeaways
- Prisma generates a fully type-safe client from your schema — every query, filter, and relation is statically typed.
- The global singleton pattern is essential in Next.js and other hot-reload environments to avoid connection exhaustion.
- Use
$transaction([...])for sequential queries and$transaction(async (tx) => {...})for interactive transactions that need to read within the transaction. Prisma.sqltagged template literals are the only safe way to write raw queries — never interpolate user input directly.selectoutperformsincludewhen you need a subset of columns — it generates leaner SQL.npx prisma migrate deployis for production;npx prisma migrate devis for local development only.- Prisma Accelerate provides connection pooling and caching as a managed service — important for serverless deployments.
- Log Prisma queries in development (
log: ['query']) to catch N+1 problems before they hit production.
Advertisement