Why Prisma Needs Schema-First Rules
Prisma's power is its schema-first approach: define models in schema.prisma, generate a type-safe client, and write queries that are validated at compile time. AI assistants skip this entirely — generating raw SQL, Sequelize-style code, or Prisma queries that don't use the type-safe features (selecting all fields, ignoring relations, no transactions for multi-step operations).
The result is Prisma code that carries the ORM's overhead without using its type safety. The worst pattern: AI generates prisma.$queryRaw for queries the Prisma client handles natively — raw SQL with no type safety, no relation awareness, and no protection against SQL injection when user input is involved.
These rules target Prisma 5+ with TypeScript. They cover schema design, client queries, relations, transactions, migrations, and seeding.
Rule 1: Schema-First Model Design
The rule: 'Define all models in schema.prisma — never create tables manually or with raw SQL. Use Prisma's relation syntax: one-to-many, many-to-many, one-to-one. Use @id for primary keys, @unique for unique constraints, @default for defaults, @map for column name mapping, @@index for composite indexes. Run npx prisma generate after every schema change to regenerate the type-safe client.'
For naming: 'Model names are PascalCase singular: User, Order, ProductCategory. Field names are camelCase: firstName, createdAt, isActive. Prisma maps to snake_case database columns automatically with @map and @@map: model User { firstName String @map("first_name") @@map("users") }.'
For relations: 'Define both sides of every relation. One-to-many: User has posts Post[], Post has author User. Many-to-many: explicit join table or implicit (Prisma manages it). Always specify onDelete behavior: @relation(... onDelete: Cascade) for dependent records, SetNull for optional relations.'
- All models in schema.prisma — never raw CREATE TABLE
- @id, @unique, @default, @map for field configuration
- @@index for composite indexes — @@map for table name mapping
- Both sides of every relation — explicit onDelete behavior
- npx prisma generate after every schema change
Rule 2: Type-Safe Client Queries
The rule: 'Use the Prisma client for all queries — never $queryRaw unless the client genuinely can't express the query (very rare: full-text search, complex CTEs). Use findUnique for single record by unique field. Use findMany with where for filtered lists. Use create, update, delete for mutations. The client is fully typed — every query returns exactly the fields in your schema.'
For select and include: 'Use select to return only needed fields: prisma.user.findMany({ select: { id: true, name: true, email: true } }). Use include for relations: prisma.user.findMany({ include: { posts: true } }). Never fetch all fields when you only need a few — select reduces payload and prevents exposing internal fields. select and include are mutually exclusive at the same level.'
For where clauses: 'Prisma provides typed filters: where: { email: { contains: "@gmail.com" } }, where: { age: { gte: 18 } }, where: { OR: [{ status: "ACTIVE" }, { role: "ADMIN" }] }. These are type-safe — the compiler catches typos in field names and invalid filter types. Never build where objects dynamically with string keys — use Prisma's typed API.'
select: { id: true, name: true, email: true } returns only those fields. Without select, Prisma returns everything — including passwordHash, internalNotes, and fields you didn't mean to expose. select is your API output filter.
Rule 3: Transactions and Batching
The rule: 'Use prisma.$transaction for operations that must be atomic: prisma.$transaction([prisma.user.create({ data: userData }), prisma.account.create({ data: accountData })]). Use the interactive transaction API for complex logic: prisma.$transaction(async (tx) => { const user = await tx.user.create(...); await tx.account.create({ data: { userId: user.id, ... } }); return user; }). Never perform multi-step mutations without a transaction — partial completion leaves data in an inconsistent state.'
For batching: 'Use createMany for bulk inserts: prisma.user.createMany({ data: users, skipDuplicates: true }). Use updateMany for bulk updates: prisma.user.updateMany({ where: { lastLoginAt: { lt: thirtyDaysAgo } }, data: { isActive: false } }). Use deleteMany for bulk deletes. Batch operations are significantly faster than individual operations in a loop.'
AI generates individual create calls in a loop without transactions — if the third create fails, the first two are committed and the data is inconsistent. $transaction ensures all-or-nothing semantics.
Three creates without $transaction: if the third fails, the first two are committed. Your database has partial, inconsistent data. $transaction ensures all-or-nothing — either all succeed or all roll back.
Rule 4: Migration Workflow
The rule: 'Use Prisma Migrate for all schema changes: npx prisma migrate dev --name description creates a migration from schema changes. Migrations are SQL files in prisma/migrations/ — committed to git, applied in order. Never use prisma db push in production — it doesn't create migration files. Use prisma migrate deploy in production CI/CD.'
For migration safety: 'Review generated SQL before applying: migrations are in prisma/migrations/[timestamp]_[name]/migration.sql. Add data migrations manually when needed (backfill columns, transform data). For destructive changes (dropping columns, changing types), use a two-phase approach: add new → migrate data → remove old.'
For seeding: 'Define seed data in prisma/seed.ts. Use prisma db seed to run. Seed scripts use the Prisma client — same type-safe API as application code. Use upsert for idempotent seeding: prisma.user.upsert({ where: { email }, create: fullUser, update: {} }). Add to package.json: "prisma": { "seed": "tsx prisma/seed.ts" }.'
- prisma migrate dev for development — creates migration SQL files
- prisma migrate deploy for production — applies pending migrations
- Never prisma db push in production — no migration file, no rollback
- Review migration SQL — add data migrations manually when needed
- prisma db seed for seeding — upsert for idempotent seeds
prisma db push applies schema changes directly — no migration file, no rollback capability, no audit trail. Use prisma migrate deploy in production CI/CD. db push is for rapid prototyping only.
Rule 5: Prisma Patterns and Anti-Patterns
The rule: 'Use middleware for cross-cutting concerns: soft deletes (filter deleted records automatically), logging (log query timing), and audit trails (record who changed what). Use Prisma Client extensions for custom model methods: const xprisma = prisma.$extends({ model: { user: { findByEmail: (email: string) => prisma.user.findUnique({ where: { email } }) } } }).'
For N+1 prevention: 'Prisma doesn't lazy-load relations — they're only fetched when you include them. This prevents N+1 by design. But AI generates loops that call findUnique per item — manual N+1. Use findMany with where: { id: { in: ids } } for batch lookups. Use include for relation loading. Profile with prisma.$on("query", ...) to log slow queries.'
For edge cases: 'Use prisma.$executeRaw for DDL operations not supported by Prisma (custom indexes, materialized views). Use prisma.$queryRaw with Prisma.sql tagged template for type-safe raw queries: prisma.$queryRaw(Prisma.sql`SELECT * FROM users WHERE email = ${email}`) — the tagged template parameterizes automatically.'
Complete Prisma Rules Template
Consolidated rules for Prisma ORM projects.
- Schema-first: all models in schema.prisma — npx prisma generate after changes
- Type-safe client: findUnique, findMany, create, update — never $queryRaw for standard ops
- select for field filtering — include for relations — never fetch all fields
- $transaction for atomic multi-step operations — never individual creates without tx
- createMany/updateMany/deleteMany for bulk — never loop with individual operations
- prisma migrate dev for development — prisma migrate deploy for production — never db push in prod
- prisma db seed with upsert — idempotent, type-safe, in prisma/seed.ts
- Client extensions for custom methods — middleware for cross-cutting concerns