Prisma — Project conventions
Applies to Prisma 6.x and 7.x. Preview-only features are flagged inline.
Schema
- One model per domain entity. No technical tables surfaced in the schema (except sessions / tokens).
- Explicit relations via
@relation. Always name the relation when there's ambiguity. @updatedAton every model whose content can change.- String IDs: prefer
@default(uuid(7))(Prisma 5.18+) for new projects — UUIDv7 is time-ordered, which keeps B-tree indexes happy.@default(cuid())still works but cuid is in maintenance mode per the Prisma team — avoid it in new schemas. Do not use@default(ulid())— ULID is not a native Prisma generator; useuuid(7)or wire a manual@defaultviadbgeneratedif you truly need ULID. - Int IDs:
@default(autoincrement()). - Enums for fixed values (roles, statuses, visibility).
Queries
- Always use an explicit
selectorinclude. NeverfindMany()without a filter on a large table. omitfor sensitive fields (GA in Prisma 6.2+) — exclude fields likepasswordorsecretat the query level instead of manualselect:
Prefer this overconst user = await prisma.user.findUnique({ where: { id }, omit: { password: true }, });select-ing every field individually when you only want to hide one or two.- Avoid N+1: use
includewith the needed relations rather than loops callingfindUnique. - Cursor-based pagination for long lists (feed, search results). Pattern:
const items = await prisma.model.findMany({ take: limit + 1, cursor: cursor ? { id: cursor } : undefined, skip: cursor ? 1 : 0, orderBy: { createdAt: 'desc' }, }); const hasMore = items.length > limit; if (hasMore) items.pop(); typedSql⚠️ Preview feature since Prisma 5.19 — still preview as of last verified 2026-04-15. Re-check Prisma's preview-features list before adopting, because preview flags can break, rename, or get removed between minor releases. When you're comfortable pinning a Prisma version and revisiting on upgrades, enable withpreviewFeatures = ["typedSql"]in the generator block, put.sqlfiles underprisma/sql/, and call them viaprisma.$queryRawTyped()— this gives type-safe raw SQL without string interpolation. On a production codebase that can't absorb preview churn, stay on$queryRawwith careful review instead.- Transactions for multi-model operations that must be atomic.
Migrations
npx prisma migrate dev --name short-descriptionin dev.- Never
db pushin production. Alwaysmigrate deploy. - Review the generated migration before committing — Prisma may emit unexpected
DROPs.
Seed
prisma/seed.jsorprisma/seed.ts. Idempotent: preferupsertovercreate.- Realistic seed data — no
test123.
Anti-patterns
- ❌
prisma.$queryRawwith unchecked template literals — if thetypedSqlpreview flag is acceptable for your project, prefer$queryRawTyped+.sqlfiles. If you can't depend on preview flags, stay on$queryRawbut sanitize inputs yourself and review each call carefully. Either way, only use raw SQL when the query builder can't express the query. - ❌
deleteMany()without awhere— always spell out the filter - ❌ Deeply nested writes (> 2 levels) — split into sequential transactions
- ❌ Missing
@@indexon fields that are frequently filtered