../registry/copilot-instructions/prisma.instructions.md
---
description: "Guidelines for Prisma ORM development in hexagonal architecture"
applyTo: "**/*.{ts,tsx,prisma}"
---
# Prisma ORM Development Guidelines
Use the `#githubRepo` tool with `prisma/prisma` to find relevant code snippets in the Prisma codebase.
Use the `#githubRepo` tool with `prisma/prisma` to answer questions about how Prisma is implemented.
## Base Configuration
### Installation and Setup
```bash
# Installation
npm install prisma @prisma/client
# Initialization
npx prisma init
# Client generation
npx prisma generate
# Migrations
npx prisma migrate dev --name init
npx prisma migrate deploy
# Prisma Studio
npx prisma studio
```
### File Structure
```
prisma/
├── schema.prisma # Main schema
├── migrations/ # Migration history
└── seed.ts # Seed script (optional)
src/
├── infrastructure/
│ └── database/
│ ├── prisma.ts # Client instance
│ ├── repositories/ # Repository implementations
│ └── migrations/ # Custom scripts
```
## Schema Definition
### Client Configuration
```prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```
### Base Models
```prisma
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relationships
notes Note[]
notebooks Notebook[]
@@map("users")
}
model Note {
id String @id @default(cuid())
title String
content Json // For PlateJS content
color String?
isPinned Boolean @default(false)
tags String[] // Array of strings
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relationships
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Indexes
@@index([userId])
@@index([createdAt])
@@index([isPinned])
@@map("notes")
}
```
### Advanced Types
```prisma
// Enums
enum NoteStatus {
DRAFT
PUBLISHED
ARCHIVED
}
// JSON for complex structures
model Page {
id String @id @default(cuid())
title String
content Json // PlateJS blocks
blocks Json[] // Array of blocks
@@map("pages")
}
// Many-to-Many relationships
model Article {
id String @id @default(cuid())
tags ArticleTag[]
@@map("articles")
}
model Tag {
id String @id @default(cuid())
name String @unique
articles ArticleTag[]
@@map("tags")
}
model ArticleTag {
articleId String
tagId String
article Article @relation(fields: [articleId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@id([articleId, tagId])
@@map("article_tags")
}
```
## Prisma Client and Connection
### Client Configuration
```typescript
// src/infrastructure/database/prisma.ts
import { PrismaClient } from '@/generated/prisma'
declare global {
var __prisma: PrismaClient | undefined
}
export const prisma = globalThis.__prisma || new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalThis.__prisma = prisma
}
// Graceful shutdown
process.on('beforeExit', async () => {
await prisma.$disconnect()
})
```
### Edge Runtime Configuration
```typescript
// For Vercel Edge Runtime
import { PrismaClient } from '@prisma/client/edge'
import { withAccelerate } from '@prisma/extension-accelerate'
const prisma = new PrismaClient({
datasourceUrl: env.DATABASE_URL,
}).$extends(withAccelerate())
```
## Repository Implementation
### Domain Interface (Port)
```typescript
// features/quicknotes/domain/ports/note-repository.port.ts
export interface NoteRepositoryPort {
findAll(userId: string): Promise<Note[]>
findById(id: string): Promise<Note | null>
create(data: CreateNoteDTO): Promise<Note>
update(id: string, data: UpdateNoteDTO): Promise<Note>
delete(id: string): Promise<void>
search(query: string, userId: string): Promise<Note[]>
findByTags(tags: string[], userId: string): Promise<Note[]>
}
```
### Repository Implementation
```typescript
// features/quicknotes/infrastructure/repositories/prisma-note.repository.ts
import { injectable } from 'tsyringe'
import { prisma } from '@/infrastructure/database/prisma'
import { NoteRepositoryPort } from '../../domain/ports/note-repository.port'
import { Note } from '../../domain/entities/note.entity'
import { CreateNoteDTO, UpdateNoteDTO } from '../../application/dtos'
import { PrismaNoteToDomainMapper } from '../mappers/prisma-note-to-domain.mapper'
@injectable()
export class PrismaNoteRepository implements NoteRepositoryPort {
async findAll(userId: string): Promise<Note[]> {
const notes = await prisma.note.findMany({
where: { userId },
orderBy: [
{ isPinned: 'desc' },
{ updatedAt: 'desc' }
]
})
return notes.map(PrismaNoteToDomainMapper.map)
}
async findById(id: string): Promise<Note | null> {
const note = await prisma.note.findUnique({
where: { id },
include: {
user: true // If needed
}
})
return note ? PrismaNoteToDomainMapper.map(note) : null
}
async create(data: CreateNoteDTO): Promise<Note> {
const note = await prisma.note.create({
data: {
title: data.title,
content: data.content,
color: data.color,
tags: data.tags || [],
userId: data.userId,
}
})
return PrismaNoteToDomainMapper.map(note)
}
async update(id: string, data: UpdateNoteDTO): Promise<Note> {
const note = await prisma.note.update({
where: { id },
data: {
...(data.title && { title: data.title }),
...(data.content && { content: data.content }),
...(data.color !== undefined && { color: data.color }),
...(data.tags && { tags: data.tags }),
...(data.isPinned !== undefined && { isPinned: data.isPinned }),
updatedAt: new Date(),
}
})
return PrismaNoteToDomainMapper.map(note)
}
async delete(id: string): Promise<void> {
await prisma.note.delete({
where: { id }
})
}
async search(query: string, userId: string): Promise<Note[]> {
const notes = await prisma.note.findMany({
where: {
userId,
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ content: { path: ['ops'], array_contains: [{ insert: { contains: query } }] } }
]
},
orderBy: { updatedAt: 'desc' }
})
return notes.map(PrismaNoteToDomainMapper.map)
}
async findByTags(tags: string[], userId: string): Promise<Note[]> {
const notes = await prisma.note.findMany({
where: {
userId,
tags: {
hasSome: tags
}
},
orderBy: { updatedAt: 'desc' }
})
return notes.map(PrismaNoteToDomainMapper.map)
}
}
```
## Data Mappers
### Prisma to Domain Mapper
```typescript
// features/quicknotes/infrastructure/mappers/prisma-note-to-domain.mapper.ts
import { Note as PrismaNote } from '@/generated/prisma'
import { Note } from '../../domain/entities/note.entity'
export class PrismaNoteToDomainMapper {
static map(prismaNote: PrismaNote): Note {
return new Note({
id: prismaNote.id,
title: prismaNote.title,
content: prismaNote.content,
color: prismaNote.color,
isPinned: prismaNote.isPinned,
tags: prismaNote.tags,
userId: prismaNote.userId,
createdAt: prismaNote.createdAt,
updatedAt: prismaNote.updatedAt,
})
}
}
```
## Transactions and Performance
### Transactions
```typescript
// Use transactions for complex operations
async createNotebookWithSections(data: CreateNotebookDTO): Promise<Notebook> {
return await prisma.$transaction(async (tx) => {
const notebook = await tx.notebook.create({
data: {
title: data.title,
description: data.description,
userId: data.userId,
}
})
const sections = await Promise.all(
data.sections.map((section, index) =>
tx.section.create({
data: {
title: section.title,
notebookId: notebook.id,
order: index,
}
})
)
)
return PrismaNotebookToDomainMapper.map({ ...notebook, sections })
})
}
```
### Query Optimizations
```typescript
// Include relationships selectively
async findNotebookWithSections(id: string): Promise<Notebook | null> {
const notebook = await prisma.notebook.findUnique({
where: { id },
include: {
sections: {
orderBy: { order: 'asc' },
include: {
pages: {
orderBy: { order: 'asc' },
select: {
id: true,
title: true,
createdAt: true,
// Don't include content for performance
}
}
}
}
}
})
return notebook ? PrismaNotebookToDomainMapper.map(notebook) : null
}
// Efficient pagination
async findNotesPaginated(
userId: string,
page: number = 1,
limit: number = 20
): Promise<{ notes: Note[]; total: number }> {
const [notes, total] = await Promise.all([
prisma.note.findMany({
where: { userId },
skip: (page - 1) * limit,
take: limit,
orderBy: { updatedAt: 'desc' }
}),
prisma.note.count({
where: { userId }
})
])
return {
notes: notes.map(PrismaNoteToDomainMapper.map),
total
}
}
```
## Migrations and Schema Evolution
### Migration Conventions
```bash
# Descriptive naming
npx prisma migrate dev --name add_user_preferences_table
npx prisma migrate dev --name update_note_content_structure
npx prisma migrate dev --name add_indexes_for_search
# Reset database (development)
npx prisma migrate reset
# Deploy to production
npx prisma migrate deploy
```
### Custom Migration Script
```typescript
// prisma/migrations/20241224000000_custom_data_migration/migration.sql
-- CreateTable
CREATE TABLE "user_preferences" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"theme" TEXT NOT NULL DEFAULT 'light',
"language" TEXT NOT NULL DEFAULT 'en',
CONSTRAINT "user_preferences_pkey" PRIMARY KEY ("id")
);
-- Manual data migration
-- UPDATE notes SET content = '{"type":"doc","content":[]}' WHERE content IS NULL;
```
### Schema Backup and Restore
```bash
# Schema backup
npx prisma db pull --preview-feature
# Introspect existing database
npx prisma db pull
# Generate client after changes
npx prisma generate
```
## Seed Data
### Seed Script
```typescript
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// Create test user
const testUser = await prisma.user.upsert({
where: { email: 'test@example.com' },
update: {},
create: {
email: 'test@example.com',
name: 'Test User',
},
})
// Create sample notes
const sampleNotes = [
{
title: 'Welcome Note',
content: { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Welcome to the app!' }] }] },
color: 'blue',
tags: ['welcome', 'getting-started'],
userId: testUser.id,
},
{
title: 'Todo List',
content: { type: 'doc', content: [] },
color: 'yellow',
tags: ['todo', 'tasks'],
isPinned: true,
userId: testUser.id,
},
]
for (const note of sampleNotes) {
await prisma.note.upsert({
where: { title: note.title },
update: {},
create: note,
})
}
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
```
## Testing with Prisma
### Test Database Setup
```typescript
// src/infrastructure/database/test-prisma.ts
import { PrismaClient } from '@/generated/prisma'
import { execSync } from 'child_process'
export const testPrisma = new PrismaClient({
datasources: {
db: {
url: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL
}
}
})
export async function setupTestDatabase() {
await testPrisma.$connect()
// Reset database for each test
await testPrisma.$executeRaw`TRUNCATE TABLE "notes", "users" RESTART IDENTITY CASCADE`
}
export async function teardownTestDatabase() {
await testPrisma.$disconnect()
}
```
### Repository Tests
```typescript
// features/quicknotes/infrastructure/__tests__/prisma-note.repository.spec.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { PrismaNoteRepository } from '../repositories/prisma-note.repository'
import { setupTestDatabase, teardownTestDatabase, testPrisma } from '@/infrastructure/database/test-prisma'
describe('PrismaNoteRepository', () => {
let repository: PrismaNoteRepository
let testUserId: string
beforeEach(async () => {
await setupTestDatabase()
repository = new PrismaNoteRepository()
// Create test user
const user = await testPrisma.user.create({
data: { email: 'test@example.com', name: 'Test User' }
})
testUserId = user.id
})
afterEach(async () => {
await teardownTestDatabase()
})
it('should create a note', async () => {
const noteData = {
title: 'Test Note',
content: { type: 'doc', content: [] },
tags: ['test'],
userId: testUserId,
}
const note = await repository.create(noteData)
expect(note.title).toBe('Test Note')
expect(note.userId).toBe(testUserId)
expect(note.tags).toEqual(['test'])
})
it('should find notes by user', async () => {
// Create test notes
await testPrisma.note.createMany({
data: [
{ title: 'Note 1', content: {}, userId: testUserId },
{ title: 'Note 2', content: {}, userId: testUserId },
]
})
const notes = await repository.findAll(testUserId)
expect(notes).toHaveLength(2)
expect(notes[0].title).toBe('Note 1')
})
})
```
## Mandatory Practices
### Naming Conventions
- **Models**: PascalCase (`User`, `Note`, `Notebook`)
- **Fields**: camelCase (`createdAt`, `isPinned`, `userId`)
- **Tables**: snake_case with @@map (`users`, `quick_notes`)
- **Indexes**: Descriptive (`@@index([userId], name: "idx_notes_user")`)
### Relationships
- Always define `onDelete` and `onUpdate` actions
- Use `Cascade` for parent-child dependencies
- Use `SetNull` for optional relationships
- Prefer explicit relationships over implicit ones
### Performance
- Add indexes for frequently searched fields
- Use `select` to limit returned fields
- Implement pagination in listings
- Use transactions for multi-table operations
- Configure connection pooling appropriately
### Security
- Never expose Prisma data directly in API
- Always use mappers between layers
- Validate input before queries
- Use Row Level Security when applicable
- Configure logs only for development
## Avoid
### Anti-patterns
- ❌ Using Prisma Client directly in Use Cases
- ❌ Exposing Prisma types in domain layer
- ❌ N+1 queries without include/select
- ❌ Unnecessary transactions for simple operations
- ❌ Migrations without backup in production
- ❌ Seeds with sensitive data
- ❌ Schema without appropriate validations
### Common Issues
- ❌ Not using connection pooling
- ❌ Queries without appropriate indexes
- ❌ Relationships without cascade rules
- ❌ Untested migrations
- ❌ Global client without singleton pattern
## Useful Commands
```bash
# Regenerate client after changes
npx prisma generate
# Visualize database
npx prisma studio
# Complete reset (development)
npx prisma migrate reset
# Deploy migrations (production)
npx prisma migrate deploy
# Validate schema
npx prisma validate
# Format schema
npx prisma format
# Introspect database
npx prisma db pull
# Push schema without migration
npx prisma db push
# Seed database
npx prisma db seed
```
## Next.js 15 Integration
### Server Actions
```typescript
// app/api/notes/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { container } from '@/core/di/container'
import { CreateNoteUseCase } from '@/features/quicknotes/application/use-cases/create-note.use-case'
export async function createNoteAction(data: CreateNoteDTO) {
const createNoteUseCase = container.resolve(CreateNoteUseCase)
try {
const note = await createNoteUseCase.execute(data)
revalidatePath('/notes')
return { success: true, data: note }
} catch (error) {
return { success: false, error: error.message }
}
}
```
### Edge Runtime
For edge functions, use `@prisma/client/edge` with Prisma Accelerate:
```typescript
import { PrismaClient } from '@prisma/client/edge'
import { withAccelerate } from '@prisma/extension-accelerate'
const prisma = new PrismaClient().$extends(withAccelerate())
export const runtime = 'edge'
```
Official reference: [Prisma Documentation](https://www.prisma.io/docs)
Description
Guidelines for Prisma ORM development in hexagonal architecture