mcpbeat Sign in

Prisma Orm Skill for Cursor

Use when modeling data or writing type-safe queries with Prisma ORM in TypeScript — `schema.prisma`, `prisma.config.ts`, the generated Prisma Client, and Prisma Migrate, including the v6 to v7 upgrade. NOT schema-as-TS with a SQL builder (that is `drizzle-orm`), NOT ORM-agnostic zero-downtime migration (that is `db-migrations`), NOT Postgres engine tuning (that is `postgresdb`).

9k tokens
context cost
the whole folder, loaded on every use
6
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill prisma-orm

What comes with it

23 784 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/migrations-and-v7-upgrade.md
references/queries-and-performance.md
scripts/verify.sh

The instruction itself

11 sections, as written by the author

Prisma ORM (v7)

You declare a data model in schema.prisma, generate a type-safe client, and query it

without the foot-guns: N+1, over-fetch, unbounded reads. This skill is Prisma 7-native.

If you are writing prisma-client-js, binaryTargets, or new PrismaClient() with no

adapter, you are writing v6 — stop and read the upgrade notes.

Decide first

Prisma is a schema DSL (schema.prisma) + a generated type-safe client + Prisma Migrate.

The rival, Drizzle, is schema-as-TypeScript with a SQL builder and no codegen — if the

ask says pgTable, drizzle-kit, or drizzle.config.ts, that is ../drizzle-orm/SKILL.md,

not here.

Prisma 7 has four non-negotiables. Get these wrong and nothing runs:

  • Generator is provider = "prisma-client" and output is REQUIRED. The client is no

longer emitted into node_modules — you import it from your generated path. The legacy

prisma-client-js generator is gone.

  • A driver adapter is REQUIRED on the constructor. new PrismaClient({ adapter }). Omit

it and you get requires either adapter or accelerateUrl at runtime.

  • Connection config lives in prisma.config.ts, not the datasource block. url,

directUrl, shadowDatabaseUrl in schema.prisma are deprecated in v7.

  • Rust-free by default. The query engine is TypeScript now — no native binaries, ~3x

faster queries, ~90% smaller bundles. binaryTargets no longer exists.

Version note: stable is the 7.x line — latest stable is 7.7.0 (2026-04-07) per the

Prisma changelog (accessed 2026-06-02); pin >=7.7.

"Prisma Next" — the future fully-TS foundation, first announced 2026-03-04 in

The Next Evolution of Prisma ORM

— reached Early Access in May 2026 per the

Prisma Next Roadmap (2026-03-20, accessed

2026-06-02). Early Access is forward-looking only and not the install default; for

production work stay on stable 7.x.

Install & config

Install the CLI, the client, and the adapter for your database. One adapter per DB.

# Postgres
npm i -D prisma
npm i @prisma/client @prisma/adapter-pg
# others: @prisma/adapter-better-sqlite3, -libsql, -mariadb, -mssql, -d1

Minimal schema.prisma — note the generator shape and the no-url datasource:

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma" // REQUIRED in v7
}

datasource db {
  provider = "postgresql" // url moved to prisma.config.ts
}

prisma.config.ts holds the connection (replaces url in the datasource):

import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: { seed: "tsx prisma/seed.ts" },
  datasource: { url: process.env.DATABASE_URL! },
});

package.json scripts:

{
  "scripts": {
    "db:generate": "prisma generate",
    "db:migrate": "prisma migrate dev",
    "db:deploy": "prisma migrate deploy",
    "db:seed": "prisma db seed"
  }
}

Model the schema

schema.prisma is the single source of truth. Every relation needs an explicit FK field and

an index on that FK column — Prisma does not index foreign keys for you, and a join on an

unindexed column is a sequential scan.

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  role      Role     @default(MEMBER)
  posts     Post[]
}

model Post {
  id        String   @id @default(cuid())
  title     String
  body      String   @db.Text // native type
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())

  @@index([authorId])            // index the FK you join/filter on
  @@unique([authorId, title])    // composite uniqueness
}

enum Role {
  ADMIN
  MEMBER
}
// Bad: relation with no index on the FK — every join scans the table
model Post {
  author   User   @relation(fields: [authorId], references: [id])
  authorId String
}
// Good: add @@index([authorId]) — see the model above.

Instantiate the client

One PrismaClient per process. A new client per request opens a fresh connection pool

each time and exhausts the database. In dev, pin it to globalThis so HMR reuses one instance.

import { PrismaClient } from "../generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({ adapter, log: ["query"] }); // log surfaces emitted SQL

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
// Bad: no adapter → throws "requires either adapter or accelerateUrl"
export const prisma = new PrismaClient();
// Bad: per-request instance → connection-pool exhaustion
app.get("/u", () => new PrismaClient({ adapter }).user.findMany());

Query safely

Three rules cover most query bugs:

  • Prefer select over include. select returns only the named fields; include adds a

relation but otherwise returns all scalar fields (over-fetch). Never put select and

include at the same level — Prisma rejects it.

  • Always pass take to findMany. It is unbounded by default and will happily stream the

whole table.

  • Never query inside a loop. Batch with where: { id: { in: ids } }.
// Good: shape the row, bound the read
const users = await prisma.user.findMany({
  take: 20,
  where: { role: "MEMBER" },
  select: { id: true, email: true, posts: { select: { title: true } } },
});
// Bad: N queries in a loop
for (const id of ids) {
  await prisma.user.findUnique({ where: { id } });
}
// Good: one query
const users = await prisma.user.findMany({ where: { id: { in: ids } } });

Pagination — pick by table size:

| Need | Use | Why |

| --- | --- | --- |

| Small/medium set, jump to page N | skip + take (offset) | Simple; skip gets slow on huge offsets |

| Large set, infinite scroll, stable | cursor + take | Seeks by indexed id, constant cost |

Writes: upsert for create-or-update; createMany({ data, skipDuplicates: true }) for bulk.

Transactions — pick by dependency:

| Situation | Form |

| --- | --- |

| Independent writes that must all commit | prisma.$transaction([opA, opB]) (array) |

| Read, branch on the result, then write | prisma.$transaction(async (tx) => { … }) (interactive) |

Lean on generated types instead of hand-writing shapes: Prisma.UserGetPayload<…> and

Prisma.validator keep a query and its type in sync. See

references/queries-and-performance.md for the full cookbook.

Performance: kill N+1 and over-fetch

relationLoadStrategy decides how relations load:

  • "join" (default) — one query: a LATERAL JOIN with JSON aggregation on Postgres (correlated

subqueries on MySQL). This is the N+1 killer. The top-level choice cascades to nested loads.

  • "query" — one query per relation, joined in the app. Sometimes wins when a join fans out

rows badly.

const feed = await prisma.user.findMany({
  relationLoadStrategy: "join",
  take: 20,
  select: { id: true, email: true, posts: { select: { id: true, title: true } } },
});

Then: select only the fields you render, and @@index the columns you filter and order on.

To see what Prisma actually emits, set log: ["query"] on the client and read the SQL. For

the deep perf checklist (N+1 detection, index selection, reading the plan) see

references/queries-and-performance.md. For EXPLAIN ANALYZE, index theory, and pool sizing

— that is the database engine's job: ../postgresdb/SKILL.md.

Raw SQL safely

Use raw SQL only through the tagged-template form — it parameterizes. The *Unsafe

variants concatenate strings and open SQL injection.

// Good: parameterized tagged template
const rows = await prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;

// Bad: interpolated string into the Unsafe API → SQL injection
const rows = await prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);

Compose fragments with Prisma.sql / Prisma.join. For fully type-checked results, write

.sql files and use $queryRawTyped. Safety matrix in

references/queries-and-performance.md.

Migrations

Pick the command by environment — this is the most common mix-up:

| Command | Use where | Does |

| --- | --- | --- |

| prisma migrate dev | development only | Creates a migration, applies it, regenerates the client; uses a shadow DB |

| prisma migrate deploy | CI / production | Applies pending migrations only — never generates, never resets |

| prisma db push | prototyping / spikes | Syncs schema to the DB with no migration file |

Rules with teeth:

  • Never run migrate dev against production. It can reset the database.
  • Never edit a migration that has already been applied. Create a new one.
  • A shadow DB lets migrate dev detect drift; it must be a separate, disposable database.
  • On drift or a failed migration, use prisma migrate resolve --applied|--rolled-back.
  • Seed via the seed entry in prisma.config.ts and prisma db seed.

Failed-migration recovery, baselining an existing database, and the full Prisma 6 to 7

upgrade are in references/migrations-and-v7-upgrade.md. For ORM-independent evolution

strategy (expand/contract, dual-write, zero-downtime backfills) that is

../db-migrations/SKILL.md.

Anti-patterns

| Anti-pattern | Why it bites | Do instead |

| --- | --- | --- |

| new PrismaClient() with no adapter | v7 throws requires either adapter or accelerateUrl | Pass a driver adapter |

| New PrismaClient per request | Pool exhaustion, dropped connections | One instance per process; globalThis in dev |

| prisma-client-js generator | Legacy v6 generator, removed in v7 | provider = "prisma-client" + output |

| binaryTargets in the generator | Rust-era artifact; v7 is Rust-free | Delete it |

| Unbounded findMany() | Streams the whole table | Always pass take (and cursor for paging) |

| Fat include everywhere | Over-fetches every scalar | select only the rendered fields |

| Query inside a for loop | N+1 round trips | Batch where: { id: { in: ids } } or relationLoadStrategy: "join" |

| $queryRawUnsafe(\…${x}…\) | SQL injection | Tagged $queryRaw\…${x}\ |

| migrate dev in CI/prod | Can reset the production DB | migrate deploy |

| Editing an applied migration | Drift, broken history | New migration; migrate resolve for drift |

| FK with no @@index | Joins do sequential scans | @@index([fkColumn]) |

Verify

Run scripts/verify.sh <path> to statically check emitted artifacts: it confirms the

prisma-client generator + output, an adapter on the constructor, and flags

$queryRawUnsafe injection, binaryTargets, unbounded findMany, and wrong-ORM

contamination. Read-only; no database connection.

How to use it

Copy the folder

Take ericrisco/prisma-orm from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npm. Without those the skill loads but fails at the first command.