Prisma ORM for TypeScript - Type-safe database toolkit with schema-first development, auto-generated client, migrations, relations, and Prisma Studio
Prisma ORM - Type-Safe Database Toolkit
Modern database toolkit for TypeScript with schema-first development, auto-generated type-safe client, and powerful migration system.
Quick Reference
Installation
npm install prisma @prisma/client
npx prisma init
Basic Workflow
# 1. Define schema
# Edit prisma/schema.prisma
2. Create migration
npx prisma migrate dev --name init
3. Generate client
npx prisma generate
4. Open Studio
npx prisma studio
### Core Schema Pattern
```prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}
Type-Safe CRUD
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Create
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
posts: {
create: { title: 'First Post', content: 'Hello World' }
}
},
include: { posts: true }
});
// Read with filters
const users = await prisma.user.findMany({
where: { email: { contains: '@example.com' } },
include: { posts: { where: { published: true } } },
orderBy: { createdAt: 'desc' },
take: 10
});
// Update
await prisma.user.update({
where: { id: userId },
data: { name: 'Bob' }
});
// Delete
await prisma.user.delete({ where: { id: userId } });
Schema Design Patterns
Field Types and Attributes
model Product {
id Int @id @default(autoincrement())
sku String @unique
name String
description String? // Optional field
price Decimal @db.Decimal(10, 2)
inStock Boolean @default(true)
quantity Int @default(0)
tags String[] // Array field (PostgreSQL)
metadata Json? // JSON field
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sku])
@@index([name, inStock])
}
Relations
One-to-Many:
model User {
id String @id @default(cuid())
posts Post[]
}
model Post {
id String @id @default(cuid())
author User @relation(fields: [authorId], references: [id])
authorId String
@@index([authorId])
}
Many-to-Many:
model Post {
id String @id @default(cuid())
categories Category[] @relation("PostCategories")
}
model Category {
id String @id @default(cuid())
name String @unique
posts Post[] @relation("PostCategories")
}
One-to-One:
model User {
id String @id @default(cuid())
profile Profile?
}
model Profile {
id String @id @default(cuid())
bio String
user User @relation(fields: [userId], references: [id])
userId String @unique
}
Self-Relations:
model User {
id String @id @default(cuid())
following User[] @relation("UserFollows")
followers User[] @relation("UserFollows")
}
Client Operations
Nested Writes
// Create with nested relations
const user = await prisma.user.create({
data: {
email: 'bob@example.com',
profile: {
create: { bio: 'Software Engineer' }
},
posts: {
create: [
{ title: 'Post 1', content: 'Content 1' },
{ title: 'Post 2', content: 'Content 2' }
]
}
}
});
// Update with nested operations
await prisma.user.update({
where: { id: userId },
data: {
posts: {
create: { title: 'New Post' },
update: {
where: { id: postId },
data: { published: true }
},
delete: { id: oldPostId }
}
}
});
Transactions
Sequential (Interactive):
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'alice@example.com' }
});
await tx.post.create({
data: { title: 'Post', authorId: user.id }
});
// Rollback if error thrown
if (someCondition) {
throw new Error('Rollback transaction');
}
});
Batch (Parallel):
const [deletedPosts, updatedUser] = await prisma.$transaction([
prisma.post.deleteMany({ where: { published: false } }),
prisma.user.update({
where: { id: userId },
data: { name: 'Updated' }
})
]);
Advanced Queries
Aggregations:
const result = await prisma.post.aggregate({
_count: { id: true },
_avg: { views: true },
_sum: { likes: true },
_max: { createdAt: true },
where: { published: true }
});
const grouped = await prisma.post.groupBy({
by: ['authorId'],
_count: { id: true },
_avg: { views: true },
having: { views: { _avg: { gt: 100 } } }
});
Raw SQL:
// Raw query
const users = await prisma.$queryRaw<User[]>`
SELECT * FROM "User" WHERE email LIKE ${`%${search}%`}
`;
// Execute
await prisma.$executeRaw`
UPDATE "Post" SET views = views + 1 WHERE id = ${postId}
`;
Migrations
Development Workflow
# Create and apply migration
npx prisma migrate dev --name add_user_role
# Reset database (WARNING: deletes all data)
npx prisma migrate reset
# View migration status
npx prisma migrate status
Production Deployment
# Apply pending migrations
npx prisma migrate deploy
# Generate client (in CI/CD)
npx prisma generate
Schema Prototyping
# Push schema without migrations (dev only)
npx prisma db push
# Pull schema from existing database
npx prisma db pull
Integration Patterns
Next.js App Router
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
Server Component:
// app/users/page.tsx
import { prisma } from '@/lib/prisma';
export default async function UsersPage() {
const users = await prisma.user.findMany({
include: { posts: { take: 5 } }
});
return (
<ul>
{users.map(u => (
<li key={u.id}>{u.name} - {u.posts.length} posts</li>
))}
</ul>
);
}
Server Action:
// app/actions.ts
'use server';
import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const authorId = formData.get('authorId') as string;
await prisma.post.create({
data: { title, authorId }
});
revalidatePath('/posts');
}
Node.js Middleware
import { PrismaClient } from '@prisma/client';
import express from 'express';
const app = express();
const prisma = new PrismaClient();
app.get('/users/:id', async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: req.params.id },
include: { posts: true }
});
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.listen(3000);
Performance Optimization
Query Optimization
// ❌ N+1 queries
const users = await prisma.user.findMany();
for (const user of users) {
const posts = await prisma.post.findMany({
where: { authorId: user.id }
});
}
// ✅ Single query with include
const users = await prisma.user.findMany({
include: { posts: true }
});
// ✅ Select specific fields
const users = await prisma.user.findMany({
select: { id: true, email: true, posts: { select: { title: true } } }
});
Pagination
// Cursor-based (recommended for large datasets)
const posts = await prisma.post.findMany({
take: 10,
cursor: lastPostId ? { id: lastPostId } : undefined,
skip: lastPostId ? 1 : 0,
orderBy: { createdAt: 'desc' }
});
// Offset-based (simple but slower)
const posts = await prisma.post.findMany({
skip: (page - 1) * pageSize,
take: pageSize
});
Connection Pooling
// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// Connection pool settings
directUrl = env("DIRECT_URL")
// Serverless connection limit
relationMode = "prisma" // For PlanetScale, Neon
}
# .env
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20"
Prisma Studio
# Launch visual database browser
npx prisma studio
Features:
Visual data browser and editor
Create, read, update, delete records
Filter and search data
View relations visually
Runs on localhost:5555
Prisma vs Drizzle
Feature
Prisma
Drizzle
Schema Definition
Custom DSL
TypeScript code
Type Safety
Generated types
Inferred types
Migrations
Built-in (migrate)
drizzle-kit
Query Builder
Fluent API
SQL-like builders
Relations
Automatic
Manual joins
Studio
Built-in GUI
No GUI
Bundle Size
~300kB
~50kB
Raw SQL
Supported
First-class
Edge Runtime
Limited
Full support
Learning Curve
Moderate
Steeper
Best For
Full-stack apps, rapid development, teams
Edge functions, SQL experts, bundle-sensitive
Choose Prisma when:
Team prefers schema-first development
Need visual database tools (Studio)
Want automatic relation handling
Building full-stack monoliths
Rapid prototyping and migrations
Choose Drizzle when:
Need minimal bundle size (edge functions)
Prefer SQL-like syntax
Edge runtime deployment (Cloudflare Workers)
Want full control over SQL generation
Team has strong SQL expertise
Best Practices
Singleton Pattern - Reuse PrismaClient instance (especially in dev)
Connection Management - Configure pool size for serverless
Select Specific Fields - Use select to reduce payload size
Use Transactions - For multi-step operations requiring atomicity
Index Strategically - Add @@index on frequently queried fields
Migration Discipline - Never edit migrations after deployment
Schema Versioning - Use descriptive migration names
Soft Deletes - Add deletedAt field instead of hard deletes
Validate Before Saving - Use Zod schemas before Prisma operations
Monitor Queries - Use prisma.$on('query') for logging
Common Pitfalls
❌ Creating multiple PrismaClient instances:
// WRONG - creates connection leak
function getUser() {
const prisma = new PrismaClient(); // New instance every call
return prisma.user.findMany();
}
// CORRECT - singleton pattern
const prisma = new PrismaClient();
function getUser() {
return prisma.user.findMany();
}
❌ N+1 queries:
// WRONG - multiple queries
const users = await prisma.user.findMany();
for (const user of users) {
user.posts = await prisma.post.findMany({ where: { authorId: user.id } });
}
// CORRECT - single query with include
const users = await prisma.user.findMany({ include: { posts: true } });
❌ Missing transaction for multi-step operations:
// WRONG - not atomic, can leave inconsistent state
await prisma.user.delete({ where: { id: userId } });
await prisma.post.deleteMany({ where: { authorId: userId } }); // May fail
// CORRECT - atomic transaction
await prisma.$transaction([
prisma.post.deleteMany({ where: { authorId: userId } }),
prisma.user.delete({ where: { id: userId } })
]);
Red Flags
Stop and reconsider if:
Creating new PrismaClient in request handlers
Not using transactions for multi-step operations
Missing indexes on foreign keys or frequently queried fields
Using findMany without pagination on large tables
Fetching entire objects when only specific fields needed
Not handling connection errors in production
Using migrate dev in production (use migrate deploy)
Integration with Other Skills
typescript-core: Zod validation, type safety patterns
nextjs-core: Server Actions, Server Components integration
nextjs-v16: App Router data fetching, caching
database-migration: Safe schema evolution patterns
Resources
Documentation: https://www.prisma.io/docs
Schema Reference: https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference
Client API: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference
Examples: https://github.com/prisma/prisma-examples
Related Skills
When using Prisma, these skills enhance your workflow:
drizzle: Drizzle ORM as lightweight alternative to Prisma
typescript: TypeScript best practices for Prisma generated types
nextjs: Prisma with Next.js: connection pooling, edge runtime considerations
test-driven-development: Testing Prisma models, migrations, and queries
[Full documentation available in these skills if deployed in your bundle]don't have the plugin yet? install it then click "run inline in claude" again.
added explicit decision points for missing env vars, existing databases, serverless platforms, and connection exhaustion; clarified inputs with connection pooling and DIRECT_URL setup; expanded procedure with edge cases for null-checks, singleton patterns, and migration ordering; specified output contracts for each step including file locations and validation checks; added outcome signals for 10 verifiable success states from compilation to production deployment.
Prisma is a modern, type-safe database toolkit for TypeScript that eliminates boilerplate and runtime errors through schema-first development. Use it when you need an auto-generated, fully-typed ORM with built-in migrations, relation handling, and visual database tools. Best for full-stack applications, rapid prototyping, and teams that want schema-to-code workflows without writing raw SQL.
Required:
Setup:
postgresql://user:password@host:port/dbname)?connection_limit=10&pool_timeout=20)External Dependencies:
Initialize Prisma in your project
npm install prisma @prisma/clientDefine your database schema
Create and apply initial migration
npx prisma migrate dev --name initGenerate Prisma Client
npx prisma generate (runs automatically during migrate dev)Import and instantiate PrismaClient in your code
Execute type-safe CRUD queries
Manage relations in nested writes
Wrap multi-step operations in transactions
prisma.$transaction() with sequential (async callback) or batch (array) modeApply schema changes post-deployment
npx prisma migrate dev --name add_column (interactive, safe reset option)npx prisma migrate deploy (applies pending migrations, no reset)Monitor and optimize query performance
prisma.$on('query', (e) => console.log(e.query)) for logging, use select/include to reduce payloadIf DATABASE_URL is not set:
If migration file already exists with same name:
If database already has schema (existing project):
npx prisma db pull to introspect existing tables and auto-generate schema.prismanpx prisma db push to push schema without migrations (dev only, destructive)npx prisma migrate dev for tracked, reversible changesIf using serverless/edge runtime (Cloudflare Workers, Lambda, Vercel):
If query returns empty result set:
If connection pool is exhausted (too many concurrent requests):
If schema changes break existing migrations:
If you need to query across multiple databases or tenants:
Successful schema definition:
npx prisma validate confirms schema is syntactically correctSuccessful migration:
Successful Prisma Client generation:
import { PrismaClient } from '@prisma/client' resolves without errorSuccessful query execution:
Successful transaction:
You know the skill worked when:
Schema compiles without errors - running npx prisma validate prints "The schema is valid" with no warnings
Migrations apply cleanly - running npx prisma migrate dev --name init creates a migration file and updates your database schema (check tables in your database client)
Queries return typed results - autocomplete in your IDE shows User and Post properties without @ts-ignore, and TypeScript compilation succeeds with no type errors
PrismaClient singleton initializes - importing your prisma instance and calling prisma.user.findMany() returns data matching your schema, no connection errors in logs
Nested writes persist correctly - creating a User with nested Posts in a single call results in both User and Post records in the database with correct authorId foreign key
Transactions rollback on error - throwing an error inside prisma.$transaction() prevents any database changes and returns error to caller
Migrations track in database - running npx prisma migrate status shows all applied migrations listed in the prisma_migrations table
Prisma Studio launches - running npx prisma studio opens localhost:5555 in browser and displays all your data visually with create/edit/delete UI
Production deploy applies migrations safely - running npx prisma migrate deploy in CI/CD applies only pending migrations without prompting or resetting data
Query logs appear in monitoring - adding prisma.$on('query', ...) logs every SQL statement with timing, confirming queries hit the database as expected
Credits: original skill by bobmatnyc. enriched with explicit decision points, edge cases, connection pooling considerations, serverless patterns, and outcome validation for production use.