Prisma is the most widely adopted ORM for Node.js and TypeScript in 2026 — used at companies from indie startups to Microsoft. It takes a fundamentally different approach from traditional ORMs like Sequelize or TypeORM: instead of writing model classes, you define your database schema in a .prisma schema file, and Prisma generates a fully type-safe database client from it. This guide covers everything you need to go from zero to a working application with Prisma 6.
Why Prisma Over Other ORMs?
The main competitors in the Node.js ORM space in 2026:
- Sequelize: The classic, but verbose model definitions and weak TypeScript types. Callbacks and outdated patterns.
- TypeORM: Better TypeScript support, but decorator-heavy, complex configuration, and notoriously buggy migrations.
- Drizzle ORM: A strong contender — SQL-first, lightweight, excellent TypeScript inference. Best choice if you want to stay close to raw SQL.
- Prisma: Schema-first, fully auto-generated type-safe client, excellent migration tooling, and the best developer experience for teams who want to stay abstracted from SQL.
Prisma is not always the fastest ORM (Drizzle generates more efficient SQL for complex queries), but it is consistently the most productive to work with, especially for teams and CRUD-heavy applications.
Installation
npm install prisma @prisma/client
npx prisma init
prisma init creates two files:
prisma/schema.prisma— your database schema definition.env— with aDATABASE_URLplaceholder
The Prisma Schema
The schema file defines your data models, relations, and database configuration:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // or "mysql", "sqlite", "sqlserver", "mongodb"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
profile Profile?
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
createdAt DateTime @default(now())
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
tags Tag[] @relation("PostToTag")
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[] @relation("PostToTag")
}
model Profile {
id Int @id @default(autoincrement())
bio String
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
enum Role {
USER
ADMIN
MODERATOR
}
Migrations
# Create and apply a migration (development)
npx prisma migrate dev --name "init"
# Prisma compares your schema against the DB, generates SQL migration file,
# applies it, and regenerates the Prisma Client
# Apply pending migrations (production)
npx prisma migrate deploy
# Reset database (wipes data!) and reapply all migrations from scratch
npx prisma migrate reset
# View migration status
npx prisma migrate status
# Open Prisma Studio (GUI database browser)
npx prisma studio
Migrations are stored as SQL files in prisma/migrations/ — commit these to git. They form an auditable history of every schema change.
Generating the Client
npx prisma generate
This regenerates the @prisma/client package based on your current schema. Run this whenever you change schema.prisma. In practice, prisma migrate dev already runs generate automatically.
Basic CRUD Operations
import { PrismaClient } from '@prisma/client'
// Create a singleton instance
const prisma = new PrismaClient()
// ── CREATE ────────────────────────────────────────────────────────────────────
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
role: 'ADMIN',
},
})
// Returns: { id: 1, email: 'alice@example.com', name: 'Alice', role: 'ADMIN', ... }
// Create with nested relation (create user + their profile in one query)
const userWithProfile = await prisma.user.create({
data: {
email: 'bob@example.com',
name: 'Bob',
profile: {
create: { bio: 'Full-stack developer' },
},
},
include: { profile: true }, // include relation in response
})
// ── READ ──────────────────────────────────────────────────────────────────────
const allUsers = await prisma.user.findMany()
const alice = await prisma.user.findUnique({
where: { email: 'alice@example.com' },
})
const admins = await prisma.user.findMany({
where: { role: 'ADMIN' },
orderBy: { createdAt: 'desc' },
take: 10, // LIMIT
skip: 20, // OFFSET
})
// Filtering
const recentPosts = await prisma.post.findMany({
where: {
published: true,
createdAt: { gte: new Date('2026-01-01') },
title: { contains: 'JavaScript', mode: 'insensitive' },
},
include: {
author: { select: { name: true, email: true } }, // select specific fields
tags: true,
},
})
// ── UPDATE ────────────────────────────────────────────────────────────────────
const updated = await prisma.user.update({
where: { id: 1 },
data: { name: 'Alice Updated', role: 'USER' },
})
// Upsert (create if not exists, update if exists)
const upserted = await prisma.user.upsert({
where: { email: 'charlie@example.com' },
update: { name: 'Charlie Updated' },
create: { email: 'charlie@example.com', name: 'Charlie' },
})
// ── DELETE ────────────────────────────────────────────────────────────────────
await prisma.user.delete({ where: { id: 1 } })
// Delete many
await prisma.post.deleteMany({
where: { published: false, createdAt: { lt: new Date('2025-01-01') } },
})
Working with Relations
// Fetch posts with their author and tags
const posts = await prisma.post.findMany({
include: {
author: true,
tags: true,
},
})
// Fetch user and count their posts
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
_count: { select: { posts: true } },
},
})
// user._count.posts = 5
// Nested create: create post for existing user
await prisma.user.update({
where: { id: 1 },
data: {
posts: {
create: {
title: 'My new post',
published: true,
},
},
},
})
// Many-to-many: connect existing tags to a post
await prisma.post.update({
where: { id: 1 },
data: {
tags: {
connect: [{ id: 1 }, { id: 2 }], // connect existing
disconnect: [{ id: 3 }], // remove tag
create: [{ name: 'new-tag' }], // create and connect
},
},
})
Transactions
// Sequential transactions (operations share the same connection)
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: 'tx@example.com' } }),
prisma.post.create({ data: { title: 'Tx post', authorId: 1 } }),
])
// Interactive transactions (full rollback on throw)
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'interactive@example.com' },
})
const post = await tx.post.create({
data: { title: 'Post', authorId: user.id },
})
// If anything throws here, both creates are rolled back
if (!post.title) throw new Error('Post creation failed')
return { user, post }
})
Raw SQL When You Need It
// Tagged template literal — safe against SQL injection
const users = await prisma.$queryRaw`
SELECT u.id, u.email, COUNT(p.id) AS post_count
FROM "User" u
LEFT JOIN "Post" p ON p."authorId" = u.id
GROUP BY u.id
ORDER BY post_count DESC
LIMIT 10
`
// Parameterized with variables
const minPosts = 5
const active = await prisma.$queryRaw`
SELECT * FROM "User" WHERE id > ${minPosts}
`
// Execute (no return value — for UPDATE, DELETE, INSERT)
await prisma.$executeRaw`
UPDATE "Post" SET published = true WHERE "createdAt" < NOW() - INTERVAL '7 days'
`
Prisma Client Best Practices
Singleton Pattern in Node.js
// lib/prisma.ts — reuse the same instance across your app
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({ log: ['query', 'error', 'warn'] })
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
In development (with hot module replacement), creating a new PrismaClient on every file reload exhausts the database connection pool. This pattern reuses the instance across hot reloads by storing it on the global object.
Selecting Only What You Need
// Don't fetch everything when you only need a few fields
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
// password hash, createdAt, etc. NOT included
},
})
Pagination
async function getUsers(page: number, pageSize = 20) {
const [users, total] = await prisma.$transaction([
prisma.user.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: 'desc' },
}),
prisma.user.count(),
])
return {
data: users,
total,
page,
totalPages: Math.ceil(total / pageSize),
}
}
Using Prisma with Next.js
// app/api/users/route.ts (Next.js App Router)
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const users = await prisma.user.findMany({
select: { id: true, name: true, email: true },
orderBy: { createdAt: 'desc' },
})
return NextResponse.json(users)
}
export async function POST(req: Request) {
const { email, name } = await req.json()
const user = await prisma.user.create({ data: { email, name } })
return NextResponse.json(user, { status: 201 })
}
Summary
Prisma's schema-first approach, auto-generated type-safe client, and excellent migration tooling make it the easiest ORM to work with in Node.js and TypeScript. Start with prisma init, define your models, run prisma migrate dev, and you have a fully typed database client in minutes. The learning curve is front-loaded in understanding the schema syntax, but once past that, querying your database is fast, readable, and safe by default.
CREESOL