Skip to main content
友田 陽大
Prisma ORM
TypeScript
Prisma
PostgreSQL
型安全
アーキテクチャ設計

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
Reading time
23 min read
Author
友田 陽大
Share

"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 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.prismaprisma 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.

Areav6 (old)v7 (current)
generatorprisma-client-js (generated into node_modules)prisma-client (make the output target explicit with output)
Query engineBundled a Rust binaryRust-free (TypeScript/WASM query compiler)
driver adapterOptional (Preview)Mandatory for all DBs (@prisma/adapter-pg for PostgreSQL)
Middleware $usePresentRemoved → Client Extensions ($extends)
Prisma.validatorPresentTreated as legacy → TypeScript's satisfies
Where config livespackage.json's "prisma" keyprisma.config.ts (defineConfig)
import source@prisma/clientThe 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 (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.

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

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

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

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.

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.

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

// 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").

// 親ユーザーと記事を同時に作成
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)

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)

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.

// ✅ 安全:${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.

// ❌ 厳禁:文字列連結はインジェクションの穴。ユーザー入力を絶対に渡さない
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.

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.

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

# 開発:差分から新しいマイグレーションを生成して即適用(+クライアント再生成)
npx prisma migrate dev --name add_user_role

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

# 試作のみ:マイグレーション履歴を残さずスキーマをDBへ同期(使い捨て)
npx prisma db push
CommandPurposeImportant property
migrate devDev onlyNeeds a shadow DB to detect diffs. On drift detection it can reset the DB (= discard data). Must never be run in production
migrate deployProduction/CIApplies only pending migrations. Safe because it uses no drift check, reset, or shadow DB
db pushPrototyping, schema experimentsDoesn'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.

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

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

AspectPrismaDrizzle
How to write the schemaCustom DSL (schema.prisma). Declarative and readableTypeScript (pgTable). No extra thing to learn
Abstraction levelHigher. Relation fetching is declarative and easyLower. Close to SQL, high transparency
Source of typesSchema → generated by prisma generateSchema (TS) → inferred with $inferSelect
MigrationsPrisma Migrate (mature, thick operational features)drizzle-kit (generate→apply is simple)
Edge / lightnessGreatly improved by going Rust-free in v7Lightweight from the start, thin dependencies
Managed integrationPrisma Postgres / Accelerate are strongAdapters 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 includedPrisma.
  • You want to keep SQL transparency, minimize dependencies, and finish with inference rather than generated artifactsDrizzle.

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

  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 devSQL 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.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading