Claude Code Prisma CLAUDE.md AI Coding TypeScript ORM 2026

CLAUDE.md for Prisma ORM: Migration Safety, Connection Pooling, and Client Extensions (2026)

The Prompt Shelf ·

Prisma’s failure modes don’t change based on which framework it’s wired into. A PrismaClient instantiated wrong breaks the same way in Next.js, NestJS, or a bare Node script. A migration run with the wrong command corrupts history the same way regardless of what’s calling it. That’s exactly why a Prisma-specific CLAUDE.md section is worth writing once — most framework guides mention Prisma in passing, but the ORM-level rules that actually prevent outages rarely get their own space.

Out of the box, Claude Code or Cursor working on a Prisma codebase will:

  • Instantiate new PrismaClient() at the top of a file instead of using a singleton, which exhausts your local Postgres connection limit within a few hot reloads
  • Reach for prisma db push because it’s the fastest path to “it works,” even on a project with an established migrations/ directory
  • Fetch a list, then fetch each item’s relations in a loop, instead of using include — a query pattern that passes code review and only shows up as a problem under real data volume
  • Edit an already-applied migration file directly instead of creating a new one
  • Skip transactions on multi-step writes that need to succeed or fail together

None of this is a Prisma limitation. It’s a documentation gap — the same one that shows up whether Prisma sits behind Next.js Server Actions, a NestJS module, or an Express route.

Why Prisma Needs Its Own CLAUDE.md Section

Framework guides — Next.js, NestJS, Express — tend to mention Prisma as one integration detail among many. That’s reasonable for a framework guide, but it means the Prisma-specific rules that actually prevent incidents get one paragraph instead of the attention they need.

Three reasons to document Prisma on its own terms:

The client lifecycle bug is framework-agnostic but framework-triggered. The singleton pattern exists specifically because of how Next.js hot reloading interacts with module-level code — but the same underlying mistake (creating a new PrismaClient per request instead of reusing one) also happens in Express apps that instantiate a client inside a route handler. The rule is universal even though the trigger differs.

Migration safety is the highest-cost mistake in the entire stack. A bad Next.js routing decision is a bug. A bad Prisma migration that ran against production is a data-loss incident. This asymmetry means migration rules deserve more explicit, more repeated emphasis than most other conventions in your CLAUDE.md.

Prisma’s query API makes the wrong pattern easy to write. include and nested select exist specifically to avoid N+1 queries, but a .map() over a list with an await inside it type-checks fine and looks like normal async code. Nothing about the language or the ORM’s types stops an agent from writing it.

Schema Conventions

Prisma schema conventions aren’t enforced by the tool — schema.prisma accepts inconsistent naming without complaint, so document the actual convention your project follows.

## schema.prisma Conventions

Models: PascalCase, singular (`User`, not `Users`).
Fields: camelCase (`createdAt`, not `created_at`).
Database table/column names: snake_case via `@@map` / `@map` — the Prisma
Client API stays camelCase, the actual SQL stays snake_case.

```prisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  fullName  String   @map("full_name")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  posts     Post[]

  @@map("users")
}

model Post {
  id        String   @id @default(cuid())
  title     String
  published Boolean  @default(false)
  authorId  String   @map("author_id")
  author    User     @relation(fields: [authorId], references: [id])

  @@map("posts")
  @@index([authorId])
}

Every foreign key field gets an explicit @@index. Prisma does not add one automatically for relation scalar fields on most databases.


## The PrismaClient Singleton Pattern

This is the single most common bug in agent-generated Prisma code, and it only shows up after several hot reloads, which makes it easy to miss in review.

```markdown
## PrismaClient Instantiation

Never call `new PrismaClient()` outside of `lib/prisma.ts`. Import the shared
instance everywhere else.

```typescript
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}
// anywhere else in the app
import { prisma } from "@/lib/prisma";

const users = await prisma.user.findMany();

Why: in development, module-level code re-runs on every hot reload. Without the globalThis cache, each reload creates a new PrismaClient — and a new connection pool — without closing the previous one, exhausting the database’s connection limit within a few dozen saves. This applies to Next.js, Remix, or any framework with HMR. It is not needed in a long-running Node process without hot reload, but the pattern is harmless there too, so use it everywhere.


## Migration Workflow Safety

This is the section worth the most repetition in your `CLAUDE.md`, because the cost of getting it wrong is disproportionate to every other mistake an agent can make in a Prisma project.

```markdown
## Migrations

Three commands exist. They are not interchangeable:

- `prisma migrate dev` — local development. Creates a migration file, applies
  it, regenerates the client. Use this when changing `schema.prisma` locally.
- `prisma migrate deploy` — CI/production. Applies existing, already-committed
  migration files. Never generates new migrations.
- `prisma db push` — prototyping only. Syncs the schema directly with no
  migration history. **Do not use this on this project.** We have a
  `prisma/migrations/` directory with committed history; `db push` bypasses it
  silently and creates drift between environments.

Hard rules:
- Never edit a migration file after it has been committed and applied.
  Create a new migration instead, even to fix a typo in the previous one.
- Never run `prisma migrate reset` against a database that isn't a local dev
  instance — it drops and recreates the entire database.
- After generating a migration with `migrate dev`, show me the generated SQL
  in `prisma/migrations/<timestamp>_<name>/migration.sql` before considering
  the task done. Autogenerated migrations sometimes produce a `DROP COLUMN`
  where a rename was intended, silently discarding data.

The “show me the generated SQL” instruction matters specifically because Prisma’s migration diffing can’t always tell a rename from a drop-and-add — if you rename a field in the schema, Prisma may generate DROP COLUMN old_name followed by ADD COLUMN new_name, which is correct for the schema but destroys any existing data in that column. A human (or the agent, if told to check) needs to catch that before it runs against real data.

Transactions

## Transactions

Multi-step writes that must succeed or fail together use `$transaction`.

**Sequential (independent operations, still atomic):**
```typescript
const [user, profile] = await prisma.$transaction([
  prisma.user.create({ data: userData }),
  prisma.profile.create({ data: profileData }),
]);

Interactive (when a later step depends on an earlier step’s result):

const result = await prisma.$transaction(async (tx) => {
  const account = await tx.account.findUniqueOrThrow({ where: { id } });
  if (account.balance < amount) {
    throw new Error("Insufficient balance");
  }
  return tx.account.update({
    where: { id },
    data: { balance: { decrement: amount } },
  });
});

Inside a $transaction callback, always use the tx client passed into the callback — not the outer prisma client. Using prisma instead of tx silently runs that query outside the transaction.


That last rule catches a mistake that's easy to make and easy to miss: an agent writing an interactive transaction will sometimes call `prisma.account.update(...)` instead of `tx.account.update(...)` inside the callback, out of habit. The code runs without error — it just isn't part of the atomic transaction anymore, which defeats the entire point.

## Preventing N+1 Queries

```markdown
## Query Patterns — No N+1

Any query that needs related data fetches it in the same call via `include`
or `select`. Never call a Prisma query inside a loop or `.map()`/`.forEach()`
callback.

```typescript
// WRONG — one query per post, N+1
const posts = await prisma.post.findMany();
for (const post of posts) {
  post.author = await prisma.user.findUnique({ where: { id: post.authorId } });
}

// CORRECT — one query total
const posts = await prisma.post.findMany({
  include: { author: true },
});

For nested relations, nest include:

const posts = await prisma.post.findMany({
  include: {
    author: { select: { id: true, fullName: true } },
    comments: { include: { author: true } },
  },
});

Use select instead of include when only specific fields are needed — it reduces payload size and avoids leaking fields like password hashes that happen to be on the model.


## Prisma Client Extensions

Client extensions are the mechanism for adding reusable query logic — soft deletes, computed fields, row-level authorization — without duplicating it at every call site. Agents that don't know a project uses extensions will reimplement the same logic inline, inconsistently, at each usage.

```markdown
## Client Extensions

This project extends `PrismaClient` for soft deletes. Never query `prisma.post`
directly for user-facing reads — the base client sees deleted rows.

```typescript
// lib/prisma.ts
export const prisma = globalForPrisma.prisma ?? new PrismaClient().$extends({
  query: {
    post: {
      async findMany({ args, query }) {
        args.where = { ...args.where, deletedAt: null };
        return query(args);
      },
    },
  },
  model: {
    post: {
      async softDelete(id: string) {
        return prisma.post.update({ where: { id }, data: { deletedAt: new Date() } });
      },
    },
  },
});

Use prisma.post.softDelete(id) instead of prisma.post.delete(...) for user-facing delete actions. A hard delete is only appropriate for admin tooling or data-retention jobs, and those call sites should say so explicitly in a comment.


## Connection Pooling for Serverless and Edge

This section is specific to deployment target, and it's the piece most Prisma guides skip entirely — worth including explicitly because getting it wrong causes production incidents, not development friction.

```markdown
## Connection Pooling (Serverless / Edge)

This project deploys to [Vercel functions / Cloudflare Workers — specify
which]. Each function invocation can open a new database connection; without
pooling, a moderate traffic spike exhausts Postgres's connection limit
(typically 100 on managed instances).

We use Prisma Accelerate for connection pooling and query caching:

```typescript
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
import { withAccelerate } from "@prisma/extension-accelerate";

export const prisma = new PrismaClient().$extends(withAccelerate());

DATABASE_URL in production points to the Accelerate connection string (prisma://...), not the direct Postgres URL. The direct URL is only used for migrations, via DIRECT_URL in schema.prisma’s datasource block.

Do not add manual pg-pool or pgbouncer configuration — Accelerate replaces that layer. If deploying to a traditional long-running server instead (not serverless), Accelerate is unnecessary; Prisma’s default connection pool handles that case fine.


Getting this distinction into the file matters because the fix for a serverless connection-exhaustion incident (add Accelerate or an external pooler like PgBouncer) is completely different from the fix for the local dev hot-reload issue (the singleton pattern above) — they look like the same symptom (connection errors) but need different rules, and an agent without both documented will often apply the wrong fix.

## Seeding

```markdown
## Database Seeding

`prisma/seed.ts` runs via `prisma db seed`. Seeds are idempotent — running
the script twice does not create duplicate data.

```typescript
// prisma/seed.ts
import { prisma } from "../lib/prisma";

async function main() {
  await prisma.user.upsert({
    where: { email: "[email protected]" },
    update: {},
    create: {
      email: "[email protected]",
      fullName: "Admin User",
    },
  });
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

Use upsert, not create, for seed data — create throws on unique constraint violations if the seed script runs more than once, which happens routinely in CI and in fresh-clone onboarding.


## Multi-Framework Integration Notes

Since the same `PrismaClient` rules apply everywhere, document only the parts that actually differ by framework.

```markdown
## Framework-Specific Notes

**Next.js:** Import `prisma` from `lib/prisma.ts` in Server Components,
Server Actions, and Route Handlers directly — no additional wrapping needed.
Do not call Prisma from Client Components.

**NestJS:** `PrismaService` is a module-scoped injectable that wraps the
client and implements `onModuleInit`/`onModuleDestroy` for connection
lifecycle. Inject it via constructor, never instantiate `PrismaClient`
directly in a service.

**Express:** Import the singleton from `lib/prisma.ts` in route handlers.
There's no framework-level DI, so the shared instance discipline matters even
more — it's the only thing preventing per-request `PrismaClient` creation.

Testing

## Testing

Integration tests run against a real Postgres instance in Docker (see
`docker-compose.test.yml`), not a mocked Prisma client — Prisma's query
builder has enough surface area that a mock rarely catches real query bugs.

```typescript
// tests/setup.ts
import { execSync } from "node:child_process";
import { prisma } from "../lib/prisma";

beforeAll(() => {
  execSync("prisma migrate deploy", { env: { ...process.env, DATABASE_URL: process.env.TEST_DATABASE_URL } });
});

afterEach(async () => {
  await prisma.$transaction([
    prisma.post.deleteMany(),
    prisma.user.deleteMany(),
  ]);
});

afterAll(async () => {
  await prisma.$disconnect();
});

Unit tests for pure logic (no DB) can use vitest-mock-extended to mock PrismaClient — reserve that for logic that happens to take a Prisma client as a parameter, not for testing query correctness itself.


## CLAUDE.md vs AGENTS.md for Prisma Projects

**AGENTS.md** is the right place for the ORM rules above — they're tool-agnostic and apply identically whether Claude Code, Cursor, or Codex is generating the query. **CLAUDE.md** adds Claude Code-specific behavior: when to ask before running a migration, how much SQL detail to show, and any review-style preferences.

AGENTS.md → schema conventions, singleton pattern, migration commands, query rules CLAUDE.md → “always show migration SQL before I apply it”, explanation depth


If your project already has a framework-specific `AGENTS.md` (Next.js, NestJS), add a `## Prisma` section to it rather than maintaining a separate file — the rules above are concise enough to fit as one section.

## Complete CLAUDE.md Template

```markdown
# CLAUDE.md

## Project Overview
[Framework] with Prisma ORM. PostgreSQL. TypeScript.

## Commands
- Dev server: [framework-specific command]
- Run migration (dev): `npx prisma migrate dev`
- Apply migrations (CI/prod): `npx prisma migrate deploy`
- Generate client: `npx prisma generate`
- Open Prisma Studio: `npx prisma studio`
- Seed database: `npx prisma db seed`
- Run tests: [test command]

## PrismaClient Instantiation
Never `new PrismaClient()` outside `lib/prisma.ts`. Import the shared
singleton everywhere else — this prevents connection pool exhaustion from
hot-reload in development.

## Schema Conventions
- Models: PascalCase singular. Fields: camelCase.
- DB tables/columns: snake_case via `@@map`/`@map`.
- Every relation scalar field gets an explicit `@@index`.

## Migrations
- `migrate dev` locally, `migrate deploy` in CI/prod. Never `db push` on this
  project — we have committed migration history.
- Never edit an applied migration file. Create a new one instead.
- Show me the generated SQL before considering a migration task done —
  autogenerated diffs can produce a destructive `DROP COLUMN` where a rename
  was intended.
- Never run `migrate reset` against anything but a local dev database.

## Transactions
Multi-step writes that must be atomic use `$transaction`. Inside an
interactive transaction callback, always use the `tx` parameter — never the
outer `prisma` client, which silently runs outside the transaction.

## Query Patterns
No queries inside loops or `.map()` callbacks. Fetch related data via
`include`/`select` in the original query. Use `select` over `include` when
only specific fields are needed.

## Connection Pooling
[If serverless/edge: document Accelerate setup and DATABASE_URL vs
DIRECT_URL split. If long-running server: note that the default pool is
sufficient and no extra config is needed.]

## Seeding
`prisma/seed.ts` uses `upsert`, never `create` — seeds must be idempotent
for CI and fresh-clone onboarding.

## Testing
Integration tests run against a real test database via Docker, not a mocked
client. Test setup applies migrations with `migrate deploy` against
`TEST_DATABASE_URL`, cleans up in `afterEach`.

## What to Avoid
- `new PrismaClient()` outside the singleton file.
- `prisma db push` on this project.
- Editing applied migration files.
- Prisma queries inside loops.
- Using the outer `prisma` client instead of `tx` inside a transaction callback.
- `create()` in seed scripts (use `upsert()`).

How Different AI Tools Read These Files

Claude Code reads CLAUDE.md/AGENTS.md automatically, merging any it finds walking up the directory tree. Rules here shape the code Claude Code generates directly — document the singleton pattern once, and every subsequent Prisma import in generated code follows it without being re-asked.

Cursor reads .cursorrules by default, with optional native AGENTS.md support. For a Prisma section embedded in a larger framework AGENTS.md, either enable that setting or symlink .cursorrules → AGENTS.md to avoid maintaining two copies of the same rules.

GitHub Copilot reads .github/copilot-instructions.md and doesn’t read AGENTS.md natively. The migration-safety and singleton-pattern rules are worth copying into that file specifically — they’re the highest-cost failure modes, and Copilot won’t see them otherwise.

OpenAI Codex reads AGENTS.md natively with the same directory-tree resolution as Claude Code, so the template above applies unmodified.

Putting It Together

Prisma’s mistakes are consistent across projects in a way most framework-specific issues aren’t: the singleton pattern, migration command discipline, and N+1 prevention apply the same whether the ORM sits behind Next.js, NestJS, or a bare script. That consistency is what makes a dedicated Prisma section worth writing once and reusing, instead of re-deriving it inside every framework’s CLAUDE.md.

The two rules worth the most emphasis, because they’re the two with the highest blast radius: never instantiate PrismaClient outside the singleton, and never use db push on a project with committed migration history. Everything else in this guide — transactions, N+1 prevention, client extensions, connection pooling — matters, but those two are the ones that turn into incidents instead of code review comments when they’re missing.

Start from the template above, add the framework-specific integration notes for your stack, and fill in the connection pooling section based on your actual deployment target (serverless needs Accelerate or an external pooler; a long-running server doesn’t).

You can find the underlying Prisma rule files — including community AGENTS.md and .cursorrules examples — in our rules collection.

FAQ

Does a Prisma project need its own CLAUDE.md, separate from the framework’s?

Yes. Prisma’s failure modes — migration safety, the client singleton pattern, query conventions — are the same regardless of framework, so they’re worth documenting once, either as their own file or as a dedicated section inside a framework AGENTS.md.

Why does Claude Code use db push instead of proper migrations?

db push is the fastest path to “it works” and what most quickstarts show, so agents default to it. It skips migration history entirely, which is fine for prototyping but creates drift on any project with committed migrations and a team.

Why does Prisma exhaust database connections in development?

Hot module reloading re-executes module-level code on every save. Instantiating PrismaClient directly (not through a globalThis-cached singleton) creates a new connection pool per reload without closing the old one, exhausting a local database’s connection limit within minutes of active editing.

How do you prevent N+1 queries in Prisma?

State it explicitly: related data gets fetched via include/select in the original query, and Prisma calls are never made inside a loop or .map() callback. The wrong pattern type-checks fine and only shows up as a performance issue at real data volume.

What is Prisma Accelerate and when do you need it?

A managed connection pooler and query cache for serverless/edge deployments, where every function invocation risks opening a new DB connection. Traditional long-running servers don’t need it — Prisma’s built-in pool handles that case.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules