# Prisma ORM Production-Operations Guide (v7): Rust-Free, driverAdapters, and Type-Safe Schema through Migrations, Transactions, and Serverless

> An implementation guide to operating Prisma ORM (v7) in production. The new 'prisma-client' generator and mandatory driver adapters, type generation from the schema, CRUD, relations (avoiding N+1), transactions and idempotency, prisma migrate dev/deploy, the safe use of $queryRaw, Client Extensions, connection management for Next.js/serverless, Prisma Postgres/Accelerate, and how to choose between Drizzle and how to migrate v6→v7—all explained in real code faithful to the official documentation.

- Published: 2026-06-26
- Author: 友田 陽大
- Tags: TypeScript, Prisma, PostgreSQL, 型安全, アーキテクチャ設計
- URL: https://tomodahinata.com/en/blog/prisma-orm-production-guide-type-safe-database-v7-driver-adapters
- Category: Prisma ORM

## Key points

- Prisma v7 retires the Rust engine, and the generator moves to 'prisma-client.' You make the output target explicit with output, and a driver adapter (@prisma/adapter-pg for PostgreSQL) becomes mandatory for all DBs
- The schema is the single source of truth. Types, the client, and migrations are derived from models, and even result types are machine-generated with Prisma.UserGetPayload and satisfies (no hand-written DTOs / as casts)
- Fetch relations with include/select; no extra query is issued per row, so no N+1 appears. Use the array form and interactive form of $transaction appropriately, and keep external API calls outside the boundary
- For migrations, dev uses migrate dev (shadow DB required, destructive), and production CI uses migrate deploy (applies only pending ones). db push leaves no history and is for disposable verification only
- Always parameterize raw SQL with $queryRaw tagged templates, and never pass user input to $queryRawUnsafe/Prisma.raw. For serverless, create the client outside the handler and don't exhaust connections

---

"I want type-safe data access"—as a requirement it's one line. But the moment you try to put it into production, the things to decide multiply at once. **Where do you declare the schema? Where do the types come from? How do you avoid N+1 on relations? Where do you draw transaction boundaries? How do you generate, review, and apply migrations? How do you structurally prevent SQL injection? How do you avoid exhausting connections in serverless?**

This article is an implementation guide to operating **Prisma ORM** at **production quality** in the **v7** generation. Prisma is an ORM with the philosophy of "**making the schema the single source of truth and deriving types, the client, and migrations all from it**," widely used for its declarative data models and good developer experience. And in **v7** (November 2025), the internal architecture changed significantly—**the long-bundled Rust query engine was retired, driver adapters became mandatory, and the client generation method (generator) is new.** This article, **assuming v7**, builds on the official documentation's facts as a foundation and layers on the design philosophy I always practice: "**push runtime correctness into types as much as possible.**"

> **The rules of this article**: API specs, commands, and code are based on the **Prisma official documentation (as of June 2026, the v7 line)**. Prisma evolves actively, and generators, adapters, and CLI flags change by version (this article's `prisma-client` generator, mandatory driver adapters, and `prisma.config.ts` are all behaviors that **became the default** in v7). Always confirm the latest specs in the [official docs](https://www.prisma.io/docs/orm) before going to production. DB connection info (connection strings, credentials) is treated as **assumed to be in environment variables** (never hardcode). Places that include "Preview" features are noted.

---

## 0. Mental model: lock the ORM boundary with "types" too

Before the main topic, let me share the single axis running through this article.

In developing a type-safe subscription billing foundation, I **fully banned** `as` / `any` / `enum` / non-null assertions. Instead, I combined Union types + `satisfies` + `NeverError(value: never)` to create a state where "**if a branch is missed, compilation fails**," and validated every external-input boundary with Zod.

This philosophy—"**knock out runtime invalidity at compile time as much as possible**"—applies directly to the ORM boundary (DB access). If anything, the database is **the boundary where types are most prone to collapse** in an app. If a `SELECT *` result flows through as `any`, no matter how carefully you write the downstream logic, it's a castle on sand.

Prisma's mental model is this.

> **Declare the data model in `schema.prisma` → `prisma generate` produces a typed client corresponding to the schema → both the input and the result of a query are typed = the schema is the single source of truth.**

The schema becomes the single source of truth, and types, the client, and migrations are all derived from it. This is the core of Prisma and the reason it lines up straight with my design principles. The biggest difference between Prisma and Drizzle boils down to "**write the schema in a custom DSL (`.prisma`) or in TypeScript**" (I organize when to use which in §13 below), but the discipline of "machine-generate types from the schema" is common to both.

---

## 1. What changed in v7 (grasp this first)

The longer you've used Prisma, the more you should read here first. v7 **changed the setup**, so code in old articles won't run as-is.

| Area | v6 (old) | v7 (current) |
| --- | --- | --- |
| generator | `prisma-client-js` (generated into `node_modules`) | **`prisma-client`** (make the **output target explicit** with `output`) |
| Query engine | Bundled a Rust binary | **Rust-free** (TypeScript/WASM query compiler) |
| driver adapter | Optional (Preview) | **Mandatory for all DBs** (`@prisma/adapter-pg` for PostgreSQL) |
| Middleware `$use` | Present | **Removed** → Client Extensions (`$extends`) |
| `Prisma.validator` | Present | **Treated as legacy** → TypeScript's `satisfies` |
| Where config lives | `package.json`'s `"prisma"` key | **`prisma.config.ts`** (`defineConfig`) |
| import source | `@prisma/client` | **The output path** (e.g. `../generated/prisma/client`) |

Going Rust-free isn't merely an internal refresh; it has practical benefits for operations. According to the official blog, the new architecture makes **queries up to 3.4× faster** and shrinks the **bundle size from about 14MB → 1.6MB (about 90% smaller).** With the native-binary dependency gone, deploying to serverless/edge is dramatically simplified—the bigger the gain, the more cold start and artifact size matter in your environment.

> **Design decision**: new projects should start on v7 (`prisma-client` generator) without hesitation. Existing v6 projects should migrate the generator, adapter, and import paths following the official [v7 upgrade guide](https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7) (I summarize the key points in §14).

---

## 2. Setup: generator, driver adapter, prisma.config.ts

### 2.1 Install

Let's take PostgreSQL as the subject. In place of the now-gone query engine, you explicitly add the **driver adapter that handles connections.**

```bash
npm install @prisma/client @prisma/adapter-pg
npm install -D prisma tsx
```

### 2.2 Schema and generator

At the top of `prisma/schema.prisma`, declare the datasource and the **new generator.** In v7, `provider = "prisma-client"`, and `output` (the generation target) is **mandatory.**

```prisma
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
}

generator client {
  provider     = "prisma-client"      // v7の新generator（旧: prisma-client-js）
  output       = "../src/generated/prisma" // 生成先を明示（node_modulesには出ない）
  runtime      = "nodejs"             // deno / bun / workerd / vercel-edge なども選べる
  moduleFormat = "esm"                // esm | cjs
}
```

This is v7's biggest change. **The generated client is output not into `node_modules` but inside your repository (the `output` path).** That is, the generated artifact becomes visible "as part of your own code," and the import source becomes the **output path**, not `@prisma/client`. Being able to generate clients for Bun / Deno / Cloudflare Workers / Vercel Edge by just switching `runtime` is also a fruit of going Rust-free (= no native-binary dependency).

> **An operational tip**: the directory you specify in `output` (e.g. `src/generated/`) is **a generated artifact, so put it in `.gitignore`**, and always run `prisma generate` before the CI and deploy builds (in `postinstall` or an explicit build step). Whether to commit the artifact is a team policy, but at minimum it's important to plug "shipping to production with generation forgotten" via your build procedure.

### 2.3 prisma.config.ts (v7's config starting point)

In v7, you declare how the CLI handles schema, migrations, and seeds type-safely in **`prisma.config.ts`** (at the project root). Environment variables are **not loaded automatically**, so load them explicitly with `dotenv` or similar.

```ts
// prisma.config.ts（プロジェクト直下）
import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
    seed: "tsx prisma/seed.ts", // シードは migrations.seed 配下
  },
  datasource: {
    url: env("DATABASE_URL"),
  },
});
```

By making config into code, **reproducibility is guaranteed in monorepos and deploys**, and the ambiguity from when it depended on a string key in `package.json` disappears.

### 2.4 Creating PrismaClient (adapter mandatory)

In v7, **passing a driver adapter when creating the client is mandatory.** This becomes the "DB handle" you reuse across the whole app.

```ts
// src/db.ts
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client"; // ← 生成先からimport

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

export const prisma = new PrismaClient({ adapter });
```

Note that `PrismaClient` is imported **from the generated `./generated/prisma/client`**, not from `@prisma/client`. Articles describing v7 tend to trip up here. The direction has been reorganized so that connection-pool tuning is done with **the adapter's options** rather than the connection string's query parameters (the pool defaults were also revisited in v7).

---

## 3. Schema-as-code: declare with `model`

Prisma's schema declares "the shape of data" with `model` blocks. This is **the source of types, the client, and all migrations.**

```prisma
model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  name    String?
  role    Role     @default(USER)
  posts   Post[]
  profile Profile?
}

model Profile {
  id     Int    @id @default(autoincrement())
  bio    String
  user   User   @relation(fields: [userId], references: [id])
  userId Int    @unique
}

model Post {
  id        Int       @id @default(autoincrement())
  title     String
  published Boolean   @default(false)
  author    User      @relation(fields: [authorId], references: [id])
  authorId  Int
  comments  Comment[]
  createdAt DateTime  @default(now())

  @@index([authorId, title])
}

model Comment {
  id      Int    @id @default(autoincrement())
  content String
  post    Post   @relation(fields: [postId], references: [id])
  postId  Int

  @@map("comments") // 物理テーブル名を comments に
}

enum Role {
  USER
  ADMIN
}
```

Let me pin the reading points from a design perspective.

- **`@id @default(autoincrement())`** is the primary key and auto-numbering. `@unique` is a uniqueness constraint. The `?` of `String?` means NULL-allowed (= `name: string | null`).
- **Relations come in pairs of two.** On the `Post` side, `author User @relation(fields: [authorId], references: [id])` is the foreign-key side, and on the `User` side, `posts Post[]` is the back-reference. Adding `@unique` to `Profile.userId` expresses **one-to-one** ("one profile per user") at the type level.
- **`@@index([authorId, title])`** is a composite index, and **`@@map("comments")`** separates the model name (in code) from the physical table name (in the DB). Code is `Comment`, DB is `comments`—this separation helps when retrofitting into an existing DB.
- **`enum`** corresponds to a DB enum type and is handled type-safely like `Role.USER` (I avoid hand-written string-literal enums and make it schema-driven this way).

> **A design tip (SRP)**: the schema sticks to the single responsibility of "the shape of data." Don't mix in aggregation views or app-specific derived values; keep business logic in the app layer. If the schema bloats, Prisma also supports splitting it into multiple files.

After rewriting the schema, regenerate the typed client with **`npx prisma generate`** (the migration commands below also handle generation).

---

## 4. Type safety: "let it be inferred," don't name the result type

Here's Prisma's true value. From the schema, both the input type and the result type are **machine-generated.** **Crossing this boundary with a hand-written DTO type or an `as` cast is an act of throwing away type safety yourself.**

```ts
import { Prisma } from "./generated/prisma/client";

// 1) 入力型：スキーマの制約（idは自動採番なので不要、nameは任意）が型に反映される
const data: Prisma.UserCreateInput = {
  email: "hinata@example.com",
  posts: { create: { title: "Hello Prisma v7" } },
};

// 2) 結果型：include/select の「形」から正確な戻り型を導出する
const userWithPosts = { include: { posts: true } } satisfies Prisma.UserDefaultArgs;
type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>;
// UserWithPosts は { id; email; name: string | null; ...; posts: Post[] } に正確に推論される
```

Two points.

1. Using a generated type like **`Prisma.UserCreateInput`** makes "what you can and can't pass on INSERT" match the schema exactly. `id` can't be passed because it's `autoincrement()`, and `name` can be omitted because it's `?`—you don't need a human to keep this diff in sync forever (structurally eliminating a textbook DRY violation).
2. **`Prisma.UserGetPayload<typeof args>`** derives the **exact result type** from the "shape to fetch" specified by `include` / `select`. If you included `posts`, `posts` appears in the result type; if not, it doesn't. Combining this with `satisfies` (a native TypeScript operator) is the v7-recommended practice.

> **A v7 caveat**: the old `Prisma.validator<...>()` is **legacy (`prisma-client-js`-only)** and can't be used with the new `prisma-client` generator. Use the **`satisfies` + `GetPayload`** above instead. "**Let the boundary type be inferred; don't name it yourself**"—this is the first discipline of using Prisma.

---

## 5. CRUD: a panorama of basic operations

It's a unified API of `prisma.<model>.<operation>`. That `where` / `select` / `orderBy` / `take` / `skip` and the rest have consistent names and meanings across models is Prisma's high learning efficiency.

```ts
// create
await prisma.user.create({
  data: { email: "elsa@prisma.io", name: "Elsa" },
});

// createMany（重複は skipDuplicates で握りつぶせる）
await prisma.user.createMany({
  data: [
    { email: "bob@prisma.io", name: "Bob" },
    { email: "yewande@prisma.io", name: "Yewande" },
  ],
  skipDuplicates: true,
});

// findUnique（一意キーでの単一取得）
const user = await prisma.user.findUnique({ where: { email: "elsa@prisma.io" } });

// findMany（条件・並び・ページング・必要な列だけ）
const users = await prisma.user.findMany({
  where: { email: { endsWith: "@prisma.io" } },
  select: { id: true, email: true }, // 取りすぎ防止（コスト＆機密の最小化）
  orderBy: { id: "asc" },
  take: 10,
  skip: 20,
});

// update / upsert / delete
await prisma.user.update({
  where: { email: "elsa@prisma.io" },
  data: { name: "Elsa the Great" },
});

await prisma.user.upsert({
  where: { email: "viola@prisma.io" },
  update: { name: "Viola v2" },
  create: { email: "viola@prisma.io", name: "Viola" },
});

await prisma.user.delete({ where: { email: "bob@prisma.io" } });
```

Three practical tips.

- **Narrowing the columns you fetch with `select`** pays off both for performance and **minimizing sensitive data.** Keeping a column like `password_hash` in a state where "it won't leak if you don't `select` it" is a good default that errs on the safe side.
- **`upsert`** is the three-piece set of `where` (find), `update` (if present), `create` (if absent). This becomes the protagonist of the idempotency design below.
- **Paging** comes in two methods: `take`/`skip` (offset) and `cursor`. On large tables, offset degrades on deep pages, so choose the `cursor: { id: lastId }` + `orderBy` cursor method.

---

## 6. Relations and N+1: fetch "in one go" with include / select

The most accident-prone thing in ORMs is the **N+1 problem** (fetch a list, then pull relations one by one, and the query count balloons to the number of rows). In Prisma you fetch relations declaratively with **`include` (whole) / `select` (chosen)**, and **no extra query is issued per row**, so written plainly, no N+1 appears.

```ts
// include：関連レコードを丸ごと
const usersWithPosts = await prisma.user.findMany({
  include: { posts: true },
});

// select：ネストして、必要な列だけ
const slim = await prisma.user.findMany({
  select: {
    name: true,
    posts: { select: { title: true } },
  },
});
```

**Nested writes** are also powerful. You can create parent and child **in one transaction** (no explicit `$transaction` needed—the official docs define "a nested write performs multiple operations in a single transaction").

```ts
// 親ユーザーと記事を同時に作成
await prisma.user.create({
  data: {
    email: "vlad@prisma.io",
    posts: { create: [{ title: "First" }, { title: "Second" }] },
  },
});

// 既存を connect、無ければ作る connectOrCreate
await prisma.post.create({
  data: {
    title: "How to make croissants",
    author: {
      connectOrCreate: {
        where: { email: "viola@prisma.io" },
        create: { email: "viola@prisma.io", name: "Viola" },
      },
    },
  },
});
```

> **Advanced (Preview feature)**: there's `relationLoadStrategy: "join" | "query"` that chooses the relation-loading strategy at fetch time. `"join"` makes it **a single query** with a `LATERAL JOIN` (on PostgreSQL) etc., and `"query"` joins multiple queries on the app side. **This feature is Preview at the time of writing**, so make production adoption contingent on a feature flag and verification. Either way, since `include`/`select` don't increase queries in proportion to row count, written plainly, N+1 is avoided.

---

## 7. Transactions and idempotency: lock the boundary with code

Processing that needs "**correctness**," like payments or inventory, is guarded not by operational carefulness but by **the structure of code.** Prisma's transactions come in two lines.

### 7.1 Array form (bundle independent multiple queries atomically)

```ts
const [posts, total] = await prisma.$transaction([
  prisma.post.findMany({ where: { title: { contains: "prisma" } } }),
  prisma.post.count(),
]);
```

### 7.2 Interactive form (a sequence that branches on intermediate values)

```ts
import { Prisma } from "./generated/prisma/client";

await prisma.$transaction(
  async (tx) => {
    const sender = await tx.account.update({
      where: { email: "alice@prisma.io" },
      data: { balance: { decrement: 100 } },
    });
    if (sender.balance < 0) throw new Error("Insufficient funds"); // throwで全ロールバック
    await tx.account.update({
      where: { email: "bob@prisma.io" },
      data: { balance: { increment: 100 } },
    });
  },
  {
    maxWait: 5_000,  // トランザクション開始を待つ上限
    timeout: 10_000, // トランザクション全体の上限
    isolationLevel: Prisma.TransactionIsolationLevel.Serializable, // 競合に強い分離レベル
  },
);
```

Three disciplines you can't skip in practice.

- **Inside a transaction, always use `tx`.** If you call the outer `prisma` instead of `tx`, that operation runs outside the transaction and atomicity breaks.
- **Keep external API calls (payment providers, etc.) outside the transaction boundary.** Holding network I/O inside a transaction occupies the DB connection and locks for a long time, becoming a breeding ground for timeouts and deadlocks.
- **Build idempotency with `upsert` and uniqueness constraints.** Assuming retries and duplicate delivery, `upsert` on a `@unique` idempotency key (`idempotencyKey`, etc.) makes "the same operation arriving twice yield one operation's result." Catching and swallowing `P2002` (uniqueness violation) on a duplicate is also a standard technique (§9).

> **Related**: the design philosophy of idempotency and double-charge prevention is dug into in a separate article on payments. The thinking that supported zero double-charges in production is universal whether the ORM is Prisma, Drizzle, or even DynamoDB.

---

## 8. Raw SQL and security: always parameterize

For aggregations or DB-specific features hard to express in the ORM, you use raw SQL, but this is **the biggest entrance for SQL injection.** Prisma's safe way is **tagged templates** (`$queryRaw` / `$executeRaw`). `${}` becomes a **bound parameter (a prepared statement)**, not string concatenation, so injection structurally cannot occur.

```ts
// ✅ 安全：${email} はパラメータとして束縛される
const email = "emelie@prisma.io";
const rows = await prisma.$queryRaw`SELECT id, name FROM "User" WHERE email = ${email}`;

// ✅ IN 句は Prisma.join で安全に展開
import { Prisma } from "./generated/prisma/client";
const ids = [1, 3, 5, 10];
await prisma.$queryRaw`SELECT * FROM "User" WHERE id IN (${Prisma.join(ids)})`;

// ✅ 条件付き断片は Prisma.sql / Prisma.empty で組み立てる
const name: string | null = null;
await prisma.$queryRaw`
  SELECT * FROM "User"
  ${name ? Prisma.sql`WHERE name = ${name}` : Prisma.empty}
`;

// ✅ 件数を返す書き込み系は $executeRaw（影響行数を number で返す）
const affected: number = await prisma.$executeRaw`
  UPDATE "User" SET active = ${true} WHERE "emailValidated" = ${true}
`;
```

Conversely, here's the form you **must not** use.

```ts
// ❌ 厳禁：文字列連結はインジェクションの穴。ユーザー入力を絶対に渡さない
const input = '"x" UNION SELECT id, title FROM "Post"';
await prisma.$queryRawUnsafe("SELECT id, name FROM \"User\" WHERE name = " + input);
```

`$queryRawUnsafe` and `Prisma.raw(...)` are APIs that splice in "**an unescaped raw fragment**," and passing user input becomes a vulnerability immediately. Even when you must use them, guarantee the input is **a fixed value under your control**, and always put external input on the parameter side (`${}` / `Prisma.join`). Note that Prisma also offers **TypedSQL** for writing raw SQL type-safely, and when raw SQL is needed it's worth considering this first.

---

## 9. Error handling: branch on codes

Prisma's known errors are thrown as **`Prisma.PrismaClientKnownRequestError`**, and you can distinguish the cause by `code`. In v7, you also import the `Prisma` namespace **from the output path.**

```ts
import { Prisma } from "./generated/prisma/client";

try {
  await prisma.user.create({ data: { email: "dup@example.com" } });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === "P2002") {
      // 一意制約違反：冪等な再試行なら 200/既存を返す、UI なら「既に登録済み」
      return { ok: false, reason: "duplicate" as const };
    }
    if (e.code === "P2025") {
      // 対象レコードが無い（update/delete の前提が崩れた）
      return { ok: false, reason: "not_found" as const };
    }
  }
  throw e; // 想定外は握りつぶさず再送出（可観測性のため）
}
```

The representative codes are **`P2002` (uniqueness violation)** and **`P2025` (target record not found).** A design that **translates these into a typed result (a Union)** so that compilation fails if there's a missing `switch` case on the caller side turns error handling from "prayer" into "guarantee." Not swallowing unexpected errors either (rethrow and leave them in logs/traces) is also important for observability.

---

## 10. Client Extensions: add cross-cutting concerns type-safely (middleware removed in v7)

In v7, **middleware (`$use`) was removed**, and **Client Extensions (`$extends`)** became the official extension point. With the four kinds `model` / `client` / `query` / `result`, you can splice in adding computed columns, query logging, and the like type-safely.

```ts
// query 拡張：全モデル・全操作の所要時間をログ（可観測性）
const prisma = basePrisma.$extends({
  query: {
    $allModels: {
      async $allOperations({ model, operation, args, query }) {
        const start = performance.now();
        const result = await query(args);
        const ms = Math.round(performance.now() - start);
        console.log(JSON.stringify({ event: "db_query", model, operation, ms }));
        return result;
      },
    },
  },
});
```

Unlike middleware, Extensions **return a new client per extension**, so you can limit the scope of application and types are preserved. It's the standard practice in production operations to make logs structured logs with a correlation ID, in a form you can cross-reference with traces.

---

## 11. Migrations: separate dev and deploy correctly

What version-controls schema changes and applies them safely to production is Prisma Migrate. **Getting the division of command roles wrong loses data in production**—I'll emphasize this.

```bash
# 開発：差分から新しいマイグレーションを生成して即適用（＋クライアント再生成）
npx prisma migrate dev --name add_user_role

# 本番/CI：保留中のマイグレーションだけを順に適用（生成もリセットもしない）
npx prisma migrate deploy

# 試作のみ：マイグレーション履歴を残さずスキーマをDBへ同期（使い捨て）
npx prisma db push
```

| Command | Purpose | Important property |
| --- | --- | --- |
| `migrate dev` | **Dev only** | Needs a **shadow DB** to detect diffs. On drift detection it **can reset the DB (= discard data).** Must never be run in production |
| `migrate deploy` | **Production/CI** | Applies only pending migrations. Safe because it **uses no drift check, reset, or shadow DB** |
| `db push` | Prototyping, schema experiments | Doesn't create `_prisma_migrations` and leaves no history. **For disposable verification where version control isn't needed** |

The **shadow DB** is a temporary DB that `migrate dev` **automatically creates and deletes** to detect schema drift and data loss. It's unnecessary in production (`migrate deploy` doesn't use it), but note that you need connection privileges to create one in the dev environment / CI when generating migrations.

> **Production flow**: in dev, `migrate dev` to generate the migration file → **a human must review the generated SQL** (for destructive changes) → commit → CI applies to production with `migrate deploy`. Zero-downtime schema changes (lock-avoiding DDL, staged application of add column → backfill → add constraint) are guaranteed by inspecting the generated SQL at this review stage.

---

## 12. Serverless / Next.js: don't exhaust connections

The most common Prisma production accident is **connection exhaustion.** In serverless, function instances spin up en masse, and if each holds a DB connection, it eats up PostgreSQL's `max_connections` in no time. Going Rust-free made artifacts lighter, but **the principles of connection management are unchanged.**

### 12.1 Hot-reload countermeasure in dev (singleton)

Next.js's `next dev` tends to recreate `PrismaClient` on each reload, multiplying connections. Hold it on `globalThis` and reuse it.

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

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

const globalForPrisma = globalThis as unknown as { prisma?: ReturnType<typeof createClient> };

export const prisma = globalForPrisma.prisma ?? createClient();

// 本番ではグローバルに保持しない（コールドスタートごとに1個で十分）
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
```

### 12.2 Principles for serverless production

- **Create the client outside the handler** and reuse it across warm instances.
- **Don't call `$disconnect()` on every request.** Repeatedly establishing and tearing down connections becomes a cost instead.
- **Insert an external pooler.** In a configuration where many functions hit the DB directly, put a pooler like PgBouncer—or the Prisma Accelerate / Prisma Postgres below—in front.

### 12.3 Prisma Postgres / Accelerate (managed connection pool and cache)

**Prisma Postgres** is a managed PostgreSQL with **a built-in PgBouncer connection pool** that resolves serverless/edge connection exhaustion on the configuration side (pooling and edge support take effect automatically with a normal connection string). **Prisma Accelerate** is a global connection-pool + query-cache layer usable even with external DBs; you `$extends` `@prisma/extension-accelerate`'s `withAccelerate()` and can specify `cacheStrategy` per query.

```ts
import { withAccelerate } from "@prisma/extension-accelerate";

const prisma = basePrisma.$extends(withAccelerate());

// クエリ単位でキャッシュ（ttl=鮮度、swr=stale許容秒）
const posts = await prisma.post.findMany({
  where: { published: true },
  cacheStrategy: { ttl: 60, swr: 10 },
});
```

> **Caveat**: Accelerate's exact connection string and initialization (especially combined with v7's driver adapter) move in detail by version, so always confirm the latest procedure in the [official docs](https://www.prisma.io/docs/accelerate) before going to production. The line "use `cacheStrategy` only for cacheable reads, and don't attach it to reads that need consistency" is important.

---

## 13. Prisma or Drizzle: the axes for choosing

In the arena of "type-safe TypeScript ORMs," the one always compared is Drizzle. I use both in production, but my selection axes are simple.

| Aspect | Prisma | Drizzle |
| --- | --- | --- |
| How to write the schema | Custom DSL (`schema.prisma`). Declarative and readable | TypeScript (`pgTable`). No extra thing to learn |
| Abstraction level | Higher. Relation fetching is declarative and easy | Lower. Close to SQL, high transparency |
| Source of types | Schema → generated by `prisma generate` | Schema (TS) → inferred with `$inferSelect` |
| Migrations | Prisma Migrate (mature, thick operational features) | drizzle-kit (generate→apply is simple) |
| Edge / lightness | Greatly improved by going Rust-free in v7 | Lightweight from the start, thin dependencies |
| Managed integration | Prisma Postgres / Accelerate are strong | Adapters support each vendor's serverless DB |

**A rough guideline**:

- **You want to go to production fast with declarative data models, thick migration operations, and a managed connection pool/cache included** → **Prisma.**
- **You want to keep SQL transparency, minimize dependencies, and finish with inference rather than generated artifacts** → **Drizzle.**

Both "make the schema the single source of truth and machine-generate types," and as long as you keep **the discipline of not crossing the boundary with `as`/`any`**, you can reach production quality. Choose by your project's constraints (existing DB, the team's SQL fluency, deploy target); there's no need to make it a holy war.

---

## 14. Key points of v6 → v7 migration

The minimal checklist for upgrading an existing project to v7 (details in the official [v7 upgrade guide](https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7)).

1. **Change the generator to `prisma-client`** and **make `output` mandatory** (break away from `node_modules` generation).
2. **Introduce a driver adapter** (`@prisma/adapter-pg` for PostgreSQL) and rewrite to **`new PrismaClient({ adapter })`.**
3. **Change the import source to the output path** (`@prisma/client` → `./generated/prisma/client`). Same for the `Prisma` namespace and error types.
4. **Port `$use` middleware to Client Extensions (`$extends`).**
5. **Replace `Prisma.validator` with `satisfies` + `GetPayload`.**
6. **Move config to `prisma.config.ts` (`defineConfig`).** Also confirm that some CLI flags like `--skip-generate` / `--skip-seed` were removed.
7. Guarantee that **`prisma generate`** runs before the CI/deploy build, and apply to production with `migrate deploy`.

The iron rule of staged migration is "**first get it into a working form, then optimize.**" Fix the three points—`output`, adapter, import—and it works first; making it Extensions and introducing Accelerate can follow.

---

## 15. Production-launch checklist

Finally, the items I always confirm before putting Prisma into production.

- [ ] The generator is `prisma-client`, `output` is explicit, the artifact is in `.gitignore` + `prisma generate` is enforced in the build
- [ ] `PrismaClient` is created with a driver adapter, and is **one instance across the whole app** (outside the handler for serverless)
- [ ] Connection strings and credentials are **environment variables only** (no hardcoding, no log output)
- [ ] Result types are **inferred** with `GetPayload` / `satisfies`. Don't cross the DB boundary with `as`/`any`
- [ ] List × relation is fetched with `include`/`select`, and **whether N+1 appears** is checked in the query log
- [ ] Processing that needs "correctness" uses `$transaction` (the interactive form uses `tx` only, external I/O is outside the boundary, `isolationLevel` if needed)
- [ ] Idempotency is a `@unique` key + `upsert`, and `P2002`/`P2025` are translated into typed results
- [ ] Raw SQL is **always parameterized** with `$queryRaw` tagged templates. Don't pass user input to `$queryRawUnsafe`/`Prisma.raw`
- [ ] Migrations are dev `migrate dev` → **SQL review** → CI `migrate deploy`. Don't use `db push` in production
- [ ] Serverless has connection-exhaustion countermeasures (pooler / Prisma Postgres / Accelerate) and a clear line on `cacheStrategy` consistency
- [ ] Query durations are made structured logs with Client Extensions and correlated with traces

---

## Summary

Prisma v7, by retiring the Rust engine and making driver adapters mandatory, advanced one step into an ORM that is "**light and fast, riding serverless/edge naturally.**" The setup changed, but the essence is consistent—**make the schema the single source of truth, derive types, the client, and migrations from it, don't cross the boundary with `as`/`any`, and guarantee correctness with the structure of code (transactions, idempotency, parameterization).** Keep this discipline and Prisma becomes a data-access layer that is "fast to write and doesn't break in production."

"**I want to stand up a type-safe data layer at production quality, fast**"—if you have such a requirement, I can help turn it into implementation, from choosing Prisma/Drizzle to migration operations and serverless connection design.
