prisma
Ensures Prisma code is type-safe, performant, and architecturally clean — covering the non-obvious patterns that trip up even experienced developers.
Core Principles
- One PrismaClient per process. Multiple instances create multiple connection pools and exhaust your database connection limit.
- Enums vs client imports. Import from the generated
enums.tsin client/shared code; import fromclient.tsonly in server-side code. Never importclient.tsin client components. - Repository pattern. Keep data access logic separate from business logic. Create repository modules for complex query sets.
- Transactions for multi-step writes. Any operation that modifies more than one record should use
$transaction. - Never modify existing migrations. Treat migrations as append-only history.
When to Use
- Writing or reviewing Prisma schema models
- Building or refactoring a database access layer
- Debugging N+1 queries or performance regressions
- Setting up Prisma in a new project (especially Next.js)
Implementation Guide
Client Setup (Singleton)
The hot-reload guard is required in Next.js — without it, dev mode creates a new PrismaClient on every file change and exhausts connections:
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production")
globalForPrisma.prisma = prisma;
Enums vs Client Imports
// ✅ Client component or shared validation code
import { Role, Status } from "@prisma/client/enums";
// ✅ Server-side only (API routes, server components, background jobs)
import { prisma } from "@/lib/prisma";
import type { User } from "@prisma/client";
// ❌ Never in client components
import { PrismaClient } from "@prisma/client";
Schema Design
- Use domain-driven model names. Keep schemas normalized and DRY.
- Declare all relations explicitly with
@relation. - Implement soft delete via
deletedAt DateTime?— never hard-delete records that may be referenced. - Use Prisma's native type decorators for database-level precision.
Queries & Performance
- N+1: Use nested
includeorselectinstead of looping with separate queries. - Pagination: Use
takeandskip; add acursor-based approach for large datasets. - Field selection: Use
selectto fetch only needed fields; avoid over-fetching with blanketinclude. - Join strategy:
relationLoadStrategy("join"or"query") is available but requires enabling therelationJoinspreview feature flag."join"uses a singleLATERAL JOIN;"query"sends one query per table and joins at the application level.
Error Handling
Catch Prisma-specific errors at the repository boundary:
PrismaClientKnownRequestError— structured DB errors. Checkerror.code:P2002— unique constraint violationP2025— record not found (replaces the removedNotFoundErrorfrom Prisma 5)
PrismaClientUnknownRequestError— unstructured DB errorsPrismaClientValidationError— invalid query shape (usually a type error)
Provide user-friendly messages upstream; log the full error with context for debugging.
Migrations
- Use descriptive names:
prisma migrate dev --name add_user_email_index. - Review generated SQL before applying to production.
- Never edit existing migration files — create a new migration instead.
- Keep migrations idempotent where possible.
Review Checklist
- Is there a single PrismaClient instance with the hot-reload guard in place?
- Are enums imported from
enums.tsin any client or shared code? - Do multi-record writes use
$transaction? - Are N+1 patterns avoided (no DB calls inside loops)?
- Are
PrismaClientKnownRequestErrorcodes handled at the repository boundary? - Do migrations have descriptive names and remain unmodified after creation?