Prisma ORM skill for Claude Code
Read the SQL Prisma writes before it hits prod. Claude treats Prisma as a SQL generator it must supervise. Models get timestamptz and indexed relation fields, every generated migration.sql is read for silent drops and table rewrites, and production only ever sees migrate deploy. Queries select only needed fields with cursor pagination, transactions stay short, and the setup matches your installed Prisma major, including the Prisma 7 driver-adapter client.
- name
prisma-orm - size238 lines · ≈2,750 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds and maintains Prisma ORM data layers - schema.prisma models and relations, native column types, Prisma Migrate workflows (migrate dev, deploy, resolve, baselining, drift), query performance (select vs include, relation load strategy, cursor pagination, batching), transactions, raw and typed SQL, and connection pooling for serverless. Use when editing schema.prisma, writing Prisma Client queries, a Prisma migration fails or drifts, Prisma queries are slow, or when upgrading Prisma major versions. Try asking
- Add a comments relation to posts in schema.prisma and create the migration
- prisma migrate deploy failed halfway in production - how do I recover?
- This Prisma findMany with nested includes takes 2 seconds, speed it up
Use it when
- You are editing schema.prisma or writing Prisma Client queries
- A Prisma migration failed, drifted, or would drop data
- Prisma-backed endpoints are slow or open too many connections
- You are upgrading Prisma across a major version
Skip it when
- The project uses Drizzle - use drizzle-orm instead
- The issue is the generated SQL's plan, not Prisma usage - use sql-query-tuning
How to check the result
prisma validate and migrate status are clean, the reviewed migration SQL is in the PR, tsc passes after generate, and the query log shows the expected count and timing per request.
The common mistake it prevents
Renaming a field in schema.prisma makes Prisma emit DROP COLUMN plus ADD COLUMN, which deletes the data unless you edit the SQL to a RENAME first.
Prisma work is done well when the schema maps to the database types you actually want (not Prisma’s defaults), every migration’s SQL has been read before it runs, queries fetch only the fields and relations the caller uses, and multi-step writes are atomic. Prisma generates SQL for you; you are still responsible for that SQL.
First, identify the setup
npx prisma --version # CLI + client versions; must match
cat package.json | grep -E '"(prisma|@prisma/[a-z-]+)"'
ls prisma/ prisma.config.ts 2>/dev/null
Version-sensitive facts (check against the installed major before applying):
- Prisma 7 (late 2025) defaults to the Rust-free client: the
prisma-clientgenerator with a requiredoutputpath, a driver adapter passed toPrismaClient(e.g.@prisma/adapter-pg), connection URL configured inprisma.config.ts, and no automatic.envloading. Middleware ($use) is gone; use client extensions. - Prisma 5/6 use the
prisma-client-jsgenerator and theurl = env("DATABASE_URL")datasource field; driver adapters and the query-compiler engine were opt-in preview features in late 6.x.
Follow the project’s existing generator and import paths. Do not mix
@prisma/client imports with a custom generated output path.
// Prisma 7 style client setup (Postgres)
import { PrismaClient } from "./generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
export const prisma = new PrismaClient({ adapter });
Instantiate one client per process (a module-level singleton). In Next.js dev, cache it
on globalThis to survive hot reload.
Schema
model User {
id String @id @default(uuid(7)) @db.Uuid
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@map("users")
}
model Post {
id BigInt @id @default(autoincrement())
authorId String @map("author_id") @db.Uuid
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
title String
status PostStatus @default(DRAFT)
priceCents Int @map("price_cents")
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
@@index([authorId])
@@index([status, publishedAt(sort: Desc)])
@@map("posts")
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
Rules:
- On Postgres,
DateTimedefaults totimestamp(3)without time zone. Add@db.Timestamptz(3)to everyDateTime. - Map to snake_case tables/columns with
@@map/@mapso raw SQL and other tools stay sane; keep camelCase in TypeScript. - Index every relation scalar (
authorId). Postgres does not index FK columns, and withrelationMode = "prisma"there are no FKs at all, so indexes are mandatory. - Always set
onDeleteexplicitly on required relations. Decide Cascade vs Restrict deliberately; the default for required relations is Restrict. - Money:
Intcents orDecimal @db.Decimal(19, 4), neverFloat. - Prisma enums become native Postgres enums; removing a value later needs a migration
that rewrites the type. Use a
String+ CHECK (added in SQL) when values churn. - Run
npx prisma formatandnpx prisma validateafter edits.
Migrations
| Situation | Command |
|---|---|
| Local development: change schema, create + apply migration | npx prisma migrate dev --name add_post_status |
| Need to edit SQL before applying (CONCURRENTLY, backfill, data fixes) | npx prisma migrate dev --create-only --name x, edit migration.sql, then npx prisma migrate dev |
| CI / staging / production | npx prisma migrate deploy (applies pending, never generates or resets) |
| Check state | npx prisma migrate status |
| Migration failed in prod and was fixed manually | npx prisma migrate resolve --applied <migration_name> or --rolled-back <migration_name> |
| Existing DB adopting Prisma Migrate (baseline) | Generate 0_init SQL with prisma migrate diff from empty to the current schema, then prisma migrate resolve --applied 0_init |
| Prototyping a throwaway schema | npx prisma db push (no migration history; never on shared/prod DBs) |
| Introspect an existing DB | npx prisma db pull |
Hard rules:
- Never run
migrate devormigrate resetagainst a shared, staging, or production database:migrate devcan prompt to reset (drop all data) on drift.migrate resetis destructive and needs explicit user confirmation even locally if the DB has data the user cares about. - Read every generated
migration.sqlbefore applying. Look for:DROP COLUMN(data loss on renames, Prisma cannot detect renames),ALTER COLUMN ... TYPE(table rewrite),CREATE INDEXwithoutCONCURRENTLYon big tables,NOT NULLadded to populated columns. - For renames, edit the generated SQL to
ALTER TABLE ... RENAME COLUMNinstead of the drop/add Prisma emits, or follow expand/contract (see schema-migrations). CREATE INDEX CONCURRENTLYcannot run in a transaction block. If Prisma’s run fails with that error, keep the statement alone in its own migration, or run it out-of-band and mark the migration applied withmigrate resolve --applied.- Never edit a migration that has already been applied anywhere; add a new one.
- Commit
prisma/migrations/includingmigration_lock.toml. - Drift detection in CI: use
prisma migrate diffbetween the migrations directory and the schema, failing if the diff is non-empty. Flag names differ across versions; checknpx prisma migrate diff --help.
Query performance
| Problem | Fix |
|---|---|
| Fetching whole rows for a list view | select: { id: true, title: true } instead of default all scalars |
Deep include producing many queries or huge payloads | select nested fields; on supported versions try relationLoadStrategy: "join" (single query with LATERAL joins) and compare |
Loop of findUnique / findFirst per item (N+1) | One findMany({ where: { id: { in: ids } } }) and map in memory; findUnique calls in the same tick are auto-batched, findFirst are not |
skip: 10000 pagination | Cursor pagination: cursor: { id: lastId }, skip: 1, take: 20, orderBy: { id: "asc" } |
| Counting for every page | Count once, or estimate; _count in select for relation counts |
| Bulk inserts in a loop | createMany({ data, skipDuplicates: true }); createManyAndReturn when you need rows back (Postgres, CockroachDB, SQLite) |
| Filtering on JSON paths in hot queries | Promote to a column with an index |
| Case-insensitive search | mode: "insensitive" (Postgres ILIKE) plus a trigram or lower() index added in SQL |
Measure, do not guess:
const prisma = new PrismaClient({
adapter,
log: [{ emit: "event", level: "query" }],
});
prisma.$on("query", (e) => {
if (e.duration > 100) console.warn(`${e.duration}ms ${e.query}`);
});
Take the logged SQL to EXPLAIN (ANALYZE, BUFFERS) (see sql-query-tuning).
Transactions
// Independent writes that must all succeed: sequential (batch) transaction
await prisma.$transaction([
prisma.account.update({ where: { id: from }, data: { balance: { decrement: amt } } }),
prisma.account.update({ where: { id: to }, data: { balance: { increment: amt } } }),
]);
// Logic between reads and writes: interactive transaction
await prisma.$transaction(
async (tx) => {
const acct = await tx.account.findUniqueOrThrow({ where: { id: from } });
if (acct.balance < amt) throw new Error("insufficient funds"); // throws -> rollback
await tx.account.update({ where: { id: from }, data: { balance: { decrement: amt } } });
await tx.ledger.create({ data: { accountId: from, amount: -amt } });
},
{ isolationLevel: "Serializable", maxWait: 5000, timeout: 10000 },
);
- Interactive transactions hold a connection and DB locks for their whole duration. Never call external APIs, send emails, or await user-facing work inside one.
- Default interactive
timeoutis 5 s andmaxWait2 s; raise deliberately, not reflexively. - Use the
txclient inside the callback. Calls on the outerprismarun outside the transaction. - With
Serializable, retry on serialization failures (Prisma errorP2034) with backoff. - Prefer atomic operations (
increment, conditionalupdateMany) over read-modify-write. Optimistic concurrency:
const { count } = await prisma.doc.updateMany({
where: { id, version },
data: { body, version: { increment: 1 } },
});
if (count === 0) throw new ConflictError();
Raw SQL
prisma.$queryRaw/$executeRawwith tagged templates are parameterized. Build dynamic fragments withPrisma.sql,Prisma.join,Prisma.raw(the last only for trusted identifiers).- Never pass user input to
$queryRawUnsafe/$executeRawUnsafe. - For complex reporting queries, TypedSQL (
.sqlfiles underprisma/sql, enabled via thetypedSqlpreview feature on versions that require it) gives typed results.
Connections and serverless
- Each serverless instance opens its own pool. Put a pooler in front (PgBouncer in transaction mode, Supabase/Neon poolers, Prisma Accelerate) and keep per-instance pool size small (1-5).
- With driver adapters, pool size is configured on the adapter/driver (e.g.
pgPoolmax). With the legacy engine, useconnection_limitandpool_timeoutURL params, andpgbouncer=truebehind PgBouncer transaction mode. - Migrations need a direct (non-pooled) connection; configure a separate direct URL for the CLI.
Gotchas
BigIntfields breakJSON.stringify; convert at the API boundary.Decimalvalues are Decimal.js objects, not numbers; do not compare with===.upsertis not a nativeON CONFLICTin every case; under concurrency it can throw unique violations (P2002). Catch and retry, or use rawINSERT ... ON CONFLICT.deleteMany({})with an accidentally empty filter deletes everything. Guard filters built from optional inputs.- Implicit many-to-many tables (
_CategoryToPost) are hard to extend later; use an explicit join model when the relation may get attributes. prisma generatemust run after every schema change and in CI/build (postinstall or build script); stale clients cause “unknown field” runtime errors.
Verification
npx prisma validate && npx prisma format --check(orformatthen confirm no diff).npx prisma migrate statusshows no pending or failed migrations; generated SQL reviewed and attached to the PR.- Type-check the app (
tsc --noEmit) afterprisma generate. - Query-log the changed endpoints: number of queries per request and their durations match expectations; EXPLAIN anything over 100 ms.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.