| name | prisma-orm |
| description | Type-safe database access with Prisma ORM. Covers schema design, migrations, relations, queries, and TypeScript integration. Use when working with Prisma, database modeling, or building type-safe data layers for Node.js/TypeScript projects. |
Prisma ORM Skill
Overview
Prisma is a next-generation Node.js and TypeScript ORM that provides:
- Prisma Schema: Declarative data modeling language
- Prisma Migrate: Database migration system
- Prisma Client: Auto-generated, type-safe query builder
- Prisma Studio: GUI for database exploration
Quick Start
npm install prisma --save-dev
npm install @prisma/client
npx prisma init
npx prisma generate
npx prisma migrate dev
npx prisma db push
npx prisma studio
npx prisma db seed
Schema Design
Basic Model Structure
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // mysql, sqlite, sqlserver, mongodb
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
role Role @default(USER)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@map("users") // Custom table name
}
Field Types & Modifiers
model Example {
// Scalar types
id Int @id @default(autoincrement())
uuid String @id @default(uuid())
cuid String @id @default(cuid())
name String @db.VarChar(255)
content String @db.Text
count Int @default(0)
price Decimal @db.Decimal(10, 2)
rating Float
isActive Boolean @default(true)
data Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Optional field
deletedAt DateTime?
// Unique constraint
slug String @unique
// Composite unique
@@unique([categoryId, slug])
// Composite index
@@index([createdAt, isActive])
}
Enums
enum Role {
USER
ADMIN
MODERATOR
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
model User {
id String @id @default(cuid())
role Role @default(USER)
}
Relations
// 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], onDelete: Cascade)
userId String @unique
}
// One-to-Many
model User {
id String @id @default(cuid())
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
author User @relation(fields: [authorId], references: [id])
authorId String
}
// Many-to-Many (implicit)
model Post {
id String @id @default(cuid())
categories Category[]
}
model Category {
id String @id @default(cuid())
name String
posts Post[]
}
// Many-to-Many (explicit - for extra fields)
model Post {
id String @id @default(cuid())
tags PostTag[]
}
model Tag {
id String @id @default(cuid())
name String @unique
posts PostTag[]
}
model PostTag {
post Post @relation(fields: [postId], references: [id])
postId String
tag Tag @relation(fields: [tagId], references: [id])
tagId String
assignedAt DateTime @default(now())
assignedBy String
@@id([postId, tagId])
}
// Self-relation
model Category {
id String @id @default(cuid())
name String
parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id])
parentId String?
children Category[] @relation("CategoryHierarchy")
}
Migrations
Development Workflow
npx prisma migrate dev --name add_user_table
npx prisma migrate deploy
npx prisma migrate reset
npx prisma migrate status
npx prisma migrate resolve --applied "20240115120000_migration_name"
Migration File Structure
prisma/
├── schema.prisma
└── migrations/
├── 20240115120000_init/
│ └── migration.sql
├── 20240116080000_add_posts/
│ └── migration.sql
└── migration_lock.toml
Custom SQL in Migrations
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
ALTER TABLE "orders" ADD CONSTRAINT "positive_amount" CHECK (amount > 0);
CREATE INDEX "active_users_idx" ON "users" (email) WHERE "deletedAt" IS NULL;
Prisma Client Queries
Setup & Instantiation
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
export default prisma;
CRUD Operations
import { prisma } from './lib/prisma';
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 'John Doe',
profile: {
create: { bio: 'Hello world' },
},
},
});
const users = await prisma.user.createMany({
data: [
{ email: 'user1@example.com', name: 'User 1' },
{ email: 'user2@example.com', name: 'User 2' },
],
skipDuplicates: true,
});
const user = await prisma.user.findUnique({
where: { id: 'cuid123' },
});
const user = await prisma.user.findUniqueOrThrow({
where: { email: 'user@example.com' },
});
const user = prisma..({
: { : },
: { : },
});
users = prisma..({
: { : },
: { : },
: ,
: ,
});
user = prisma..({
: { : },
: { : },
});
user = prisma..({
: { : },
: { : },
: { : , : },
});
result = prisma..({
: { : },
: { : },
});
user = prisma..({
: { : },
});
result = prisma..({
: { : { : } },
});
Filtering
const users = await prisma.user.findMany({
where: {
age: { gt: 18 },
score: { gte: 90 },
price: { lt: 100 },
count: { lte: 10 },
status: { not: 'DELETED' },
role: { in: ['ADMIN', 'MODERATOR'] },
type: { notIn: ['SPAM', 'BOT'] },
},
});
const users = await prisma.user.findMany({
where: {
email: { contains: '@example.com' },
name: { startsWith: 'John' },
bio: { endsWith: 'developer' },
email: { contains: 'JOHN', : },
},
});
users = prisma..({
: {
: [
{ : },
{ : },
],
: [
{ : { : } },
{ : },
],
: {
: { : },
},
},
});
posts = prisma..({
: {
: {
: { : },
},
: {
: { : },
: { : },
: { : },
},
},
});
recentPosts = prisma..({
: {
: {
: (),
: (),
},
},
});
users = prisma..({
: {
: ,
: { : },
},
});
Select & Include (Relations)
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
posts: {
select: { id: true, title: true },
take: 5,
},
},
});
const user = await prisma.user.findUnique({
where: { id: 'cuid123' },
include: {
profile: true,
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10,
include: {
comments: {
take: 3,
orderBy: { createdAt: 'desc' },
},
},
},
},
});
const usersWithCounts = await prisma.user.findMany({
: {
: {
: { : , : },
},
},
});
Aggregations
const userCount = await prisma.user.count({
where: { isActive: true },
});
const stats = await prisma.order.aggregate({
_sum: { amount: true },
_avg: { amount: true },
_min: { amount: true },
_max: { amount: true },
_count: true,
where: { status: 'COMPLETED' },
});
const ordersByStatus = await prisma.order.groupBy({
by: ['status'],
_count: true,
_sum: { amount: true },
having: {
amount: { _sum: { gt: 1000 } },
},
});
const uniqueCategories = await prisma.post.findMany({
distinct: ['categoryId'],
: { : },
});
Raw Queries
const users = await prisma.$queryRaw<User[]>`
SELECT * FROM users
WHERE email LIKE ${`%@example.com`}
ORDER BY created_at DESC
LIMIT 10
`;
const email = 'user@example.com';
const user = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;
const result = await prisma.$executeRaw`
UPDATE users SET last_login = NOW() WHERE id = ${userId}
`;
import { Prisma } from '@prisma/client';
const orderBy = Prisma.sql`ORDER BY created_at DESC`;
const users = await prisma.$queryRaw`
SELECT * FROM users ${orderBy}
`;
const postsWithAuthors = await prisma.$queryRaw`
SELECT p.*, u.name as author_name
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.published = true
`;
Transactions
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: 'user@example.com' } }),
prisma.post.create({ data: { title: 'Hello', authorId: 'existing-id' } }),
]);
const result = await prisma.$transaction(async (tx) => {
const sender = await tx.account.update({
where: { id: senderId },
data: { balance: { decrement: amount } },
});
if (sender.balance < 0) {
throw new Error('Insufficient funds');
}
const recipient = await tx.account.update({
where: { id: recipientId },
data: { balance: { increment: amount } },
});
const transaction = await tx..({
: { senderId, recipientId, amount },
});
transaction;
}, {
: ,
: ,
: ..,
});
user = prisma..({
: {
: ,
: { : { : } },
: {
: [
{ : },
{ : },
],
},
},
: { : , : },
});
Connection Pooling
Configuration
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// Connection pool settings via URL params
// postgresql://user:pass@host:5432/db?connection_limit=5&pool_timeout=10
}
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
process.on('beforeExit', async () => {
await prisma.$disconnect();
});
Serverless / Edge
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
const connectionString = process.env.DATABASE_URL;
const prisma = new PrismaClient({
datasourceUrl: process.env.ACCELERATE_URL,
});
TypeScript Integration
Generated Types
import {
User,
Post,
Prisma,
Role
} from '@prisma/client';
function processUser(user: User): void {
console.log(user.email);
}
type UserCreateInput = Prisma.UserCreateInput;
type UserUpdateInput = Prisma.UserUpdateInput;
type UserWhereInput = Prisma.UserWhereInput;
type UserWhereUniqueInput = Prisma.UserWhereUniqueInput;
type UserWithPosts = Prisma.UserGetPayload<{
include: { posts: true };
}>;
type UserSummary = Prisma.UserGetPayload<{
select: {
id: true;
: ;
: ;
: { : { : } };
};
}>;
Type-Safe Service Layer
import { Prisma, User } from '@prisma/client';
import { prisma } from './lib/prisma';
class UserRepository {
async findById(id: string): Promise<User | null> {
return prisma.user.findUnique({ where: { id } });
}
async findMany(
where?: Prisma.UserWhereInput,
orderBy?: Prisma.UserOrderByWithRelationInput,
pagination?: { skip?: number; take?: number }
): Promise<User[]> {
return prisma.user.findMany({
where,
orderBy,
...pagination,
});
}
async create(data: Prisma.UserCreateInput): Promise<User> {
return prisma.user.create({ data });
}
async (: , : .): <> {
prisma..({ : { id }, data });
}
(: ): <> {
prisma..({ : { id } });
}
}
userRepository = ();
Validation with Zod
import { z } from 'zod';
import { Prisma } from '@prisma/client';
const UserCreateSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100).optional(),
role: z.enum(['USER', 'ADMIN', 'MODERATOR']).default('USER'),
}) satisfies z.ZodType<Prisma.UserCreateInput>;
function createUser(input: unknown) {
const validated = UserCreateSchema.parse(input);
return prisma.user.create({ data: validated });
}
Testing Patterns
Test Setup
import { PrismaClient } from '@prisma/client';
import { execSync } from 'child_process';
import { randomUUID } from 'crypto';
const prisma = new PrismaClient();
beforeAll(async () => {
execSync('npx prisma db push --force-reset', {
env: { ...process.env, DATABASE_URL: process.env.TEST_DATABASE_URL },
});
});
beforeEach(async () => {
const tablenames = await prisma.$queryRaw<{ tablename: string }[]>`
SELECT tablename FROM pg_tables WHERE schemaname='public'
`;
for (const { tablename } of tablenames) {
if (tablename !== '_prisma_migrations') {
await prisma.$executeRawUnsafe(
`TRUNCATE TABLE "public"."${tablename}" CASCADE;`
);
}
}
});
afterAll(async () => {
await prisma.$disconnect();
});
export { prisma };
Test Factories
import { faker } from '@faker-js/faker';
import { Prisma } from '@prisma/client';
import { prisma } from '../setup';
export function buildUser(
overrides?: Partial<Prisma.UserCreateInput>
): Prisma.UserCreateInput {
return {
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'USER',
...overrides,
};
}
export async function createUser(
overrides?: Partial<Prisma.UserCreateInput>
) {
return prisma.user.create({
data: buildUser(overrides),
});
}
export async function createUsers(count: number) {
return Promise.(
.({ : count }, ())
);
}
Integration Tests
import { prisma, createUser } from './setup';
import { userService } from '../src/services/user';
describe('UserService', () => {
describe('findByEmail', () => {
it('returns user when found', async () => {
const created = await createUser({ email: 'test@example.com' });
const found = await userService.findByEmail('test@example.com');
expect(found).toMatchObject({
id: created.id,
email: 'test@example.com',
});
});
it('returns null when not found', async () => {
const found = await userService.findByEmail('nonexistent@example.com');
expect(found).toBeNull();
});
});
describe('createWithProfile', () => {
it('creates user and profile in transaction', async () => {
const result = userService.({
: ,
: ,
: ,
});
(result.)..();
(result.?.).();
});
(, () => {
(
userService.({
: ,
: ,
: ,
})
)..();
user = prisma..({
: { : },
});
(user).();
});
});
});
Mocking Prisma
import { PrismaClient } from '@prisma/client';
import { mockDeep, DeepMockProxy } from 'jest-mock-extended';
export type MockPrismaClient = DeepMockProxy<PrismaClient>;
export const createMockPrisma = (): MockPrismaClient => {
return mockDeep<PrismaClient>();
};
import { createMockPrisma } from './mocks/prisma';
describe('UserService (unit)', () => {
const mockPrisma = createMockPrisma();
const userService = new UserService(mockPrisma);
it('calls prisma.user.findUnique', async () => {
mockPrisma.user.findUnique.mockResolvedValue({
id: '1',
email: 'test@example.com',
name: 'Test',
role: 'USER',
createdAt: new Date(),
updatedAt: (),
});
result = userService.();
(mockPrisma..).({
: { : },
});
(result?.).();
});
});
Best Practices
Performance
const users = await prisma.user.findMany({
select: { id: true, email: true },
});
const users = await prisma.user.createMany({
data: usersToCreate,
skipDuplicates: true,
});
const users = await prisma.user.findMany({
take: 10,
cursor: { id: lastUserId },
skip: 1,
});
Error Handling
import { Prisma } from '@prisma/client';
try {
await prisma.user.create({ data });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new Error('Email already exists');
}
if (error.code === 'P2025') {
throw new Error('Record not found');
}
}
throw error;
}
Soft Deletes
prisma.$use(async (params, next) => {
if (params.model === 'User') {
if (params.action === 'delete') {
params.action = 'update';
params.args.data = { deletedAt: new Date() };
}
if (params.action === 'findMany' || params.action === 'findFirst') {
params.args.where = { ...params.args.where, deletedAt: null };
}
}
return next(params);
});
Common Error Codes
| Code | Description | Solution |
|---|
| P2002 | Unique constraint violation | Handle duplicate entries |
| P2003 | Foreign key constraint | Ensure related records exist |
| P2025 | Record not found | Validate before update/delete |
| P2024 | Connection pool timeout | Increase pool size/timeout |
| P1001 | Can't reach database | Check connection string |
| P1008 | Operations timed out | Optimize query or increase timeout |