Skip to content
Back to skills

Prisma Orm

ASecurity

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`).

  • 134 stars
  • 0 votes
  • 0 copies
  • 7 views
  • Added September 2, 2026
ai-agentstypescriptrustgobashsqlnodeapidatabaseperformance

Works with

  • cursor
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 6 files and shows the line behind each finding

Scanned September 2, 2026

npx -y skills add ericrisco/rsc-harness --skill prisma-orm --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Prisma Orm?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Prisma Orm
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ericrisco-prisma-orm/badge)](https://www.skillsdirectory.com/skills/ericrisco-prisma-orm)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: prisma-orm
description: "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`)."
tags:
  - prisma
  - prisma-orm
  - typescript-orm
  - prisma-migrate
  - schema
  - migrations
  - orm
recommends:
  - drizzle-orm
  - db-migrations
  - postgresdb
  - neon
  - planetscale
  - supabase
  - sqlite-turso
  - typescript
  - sql
origin: risco
---

# 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:

1. **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.
2. **A driver adapter is REQUIRED on the constructor.** `new PrismaClient({ adapter })`. Omit
   it and you get `requires either adapter or accelerateUrl` at runtime.
3. **Connection config lives in `prisma.config.ts`, not the datasource block.** `url`,
   `directUrl`, `shadowDatabaseUrl` in `schema.prisma` are deprecated in v7.
4. **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](https://www.prisma.io/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](https://www.prisma.io/blog/the-next-evolution-of-prisma-orm)
— reached **Early Access in May 2026** per the
[Prisma Next Roadmap](https://www.prisma.io/blog/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.

```bash
# 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**:

```prisma
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):

```ts
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:

```json
{
  "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.

```prisma
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
}
```

```prisma
// 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.

```ts
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;
```

```ts
// 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 } }`.

```ts
// 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 } } },
});
```

```ts
// 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.

```ts
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.

```ts
// 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.

Files in this skill

  • SKILL.md11.7 KB
  • evals/README.md889 B
  • evals/cases.yaml3.3 KB
  • references/migrations-and-v7-upgrade.md6.7 KB
  • references/queries-and-performance.md7 KB
  • scripts/verify.sh5.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.