Skip to content

Drizzle vs Prisma: Six Months In, Here’s What I Actually Use

Drizzle vs Prisma: Six Months In, Here’s What I Actually Use

Confession: I spent two weeks last winter trying to convince myself that Prisma was still the right call for a side project I was rebuilding. The schema file looked clean, the type generation felt familiar, and I’d shipped enough Prisma apps that I could do it half-asleep. Then I deployed it to a Cloudflare Worker, watched the cold-start times, and quietly started porting the data layer to Drizzle that same night.

Six months later I’ve used both in production on three different shapes of project: a Next.js app on Vercel, a small Hono API on Workers, and a long-running Node service on a Hetzner box. What I learned isn’t ‘Drizzle wins’. It’s that the choice has stopped being about features and started being about where your code runs, how comfortable you are reading SQL, and how much you care about your migrations behaving like, well, migrations.

If you want the short answer: I default to Drizzle now, but Prisma is still the right tool for two specific cases. Read on if you want to know which two.

The shape problem: schema-first vs. code-first

Both ORMs ask you to declare your schema once and get types and queries from it. They just disagree on what ‘declare your schema’ means.

In Prisma you write a .prisma file. It’s a small DSL the team built to look like a stripped-down GraphQL schema, and you run prisma generate to turn it into a client. Here’s a tiny example:

model Post {
  id        Int      @id @default(autoincrement())
  slug      String   @unique
  title     String
  body      String
  published Boolean  @default(false)
  createdAt DateTime @default(now())
}

In Drizzle the schema is just TypeScript. No DSL, no codegen step, no separate language to learn:

import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  slug: text('slug').notNull().unique(),
  title: text('title').notNull(),
  body: text('body').notNull(),
  published: boolean('published').notNull().default(false),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

The Prisma file is shorter and reads almost like documentation. The Drizzle file is plain TS, so it composes with your existing tooling. You refactor across the codebase with the same Find References command you already use. You share types with your validation layer. You run codemods on it. After about a week of writing the TypeScript version I stopped missing the DSL. After two weeks I started liking that there was no codegen step to forget.

Migrations: where the real friction lives

This is where I switched camps for good.

Prisma’s migration story is a wrapper around SQL. You change schema.prisma, run prisma migrate dev, and it writes a migration.sql file plus a _prisma_migrations tracking table. That works fine in development. In production it gets opinionated. The CLI wants to detect drift, wants to reset on conflict, and the failure modes when you’re recovering from a partial deploy are not fun to debug at 2am.

Drizzle uses drizzle-kit to generate migrations from your TS schema, but the output is a plain SQL file you can read, edit, commit, and apply yourself. Generation looks like this:

npx drizzle-kit generate
# writes drizzle/0001_add_published_flag.sql
npx drizzle-kit migrate
# applies pending migrations using a tiny tracking table

The SQL file is the source of truth. If drizzle-kit generates something I don’t like, and it does occasionally for renames or partial unique indexes, I open the file and fix it before applying. There’s no clever drift detection trying to outsmart me. The Drizzle migrations docs describe this as ‘manual migrations with codegen’, and that label is accurate.

I’m not the only person who finds the SQL-first approach calming. Even the official Prisma migrate docs recommend an apply-only workflow in production, which is essentially what Drizzle gives you by default. The gap closes once you wire your CI up carefully. But you have to wire it up carefully, and you don’t with Drizzle.

TypeScript inference: same idea, very different output

Both give you autocompletion and end-to-end types. The shape of the output is where they part ways.

Prisma returns nested objects when you include relations:

const post = await prisma.post.findUnique({
  where: { slug: 'drizzle-vs-prisma' },
  include: { author: true, tags: true },
});
// post.author.name, post.tags[0].label

Drizzle gives you two flavors. There’s a SQL-like query builder that returns flat rows. You write the joins yourself, you read what comes back:

const rows = await db
  .select()
  .from(posts)
  .leftJoin(authors, eq(posts.authorId, authors.id))
  .where(eq(posts.slug, 'drizzle-vs-prisma'));

And there’s a relational query API that returns nested objects, much closer to Prisma’s feel:

const post = await db.query.posts.findFirst({
  where: eq(posts.slug, 'drizzle-vs-prisma'),
  with: { author: true, tags: true },
});

The relational API was the thing that made Drizzle stop feeling like a step backward. I use it for most read paths because the inference is identical to Prisma’s mental model. I drop down to the SQL builder when I want a specific join order, a CTE, a window function, or anything else where I want to see the exact query the database will run. The escape hatch is the actual point, for me.

Prisma also has raw queries via $queryRaw, but the ergonomics aren’t great and the types are looser. Drizzle’s tagged-template sql helper is genuinely pleasant to read and types parameters correctly.

Runtime cost: cold starts, bundle size, edge runtimes

This is what pushed me over the edge for the Workers deploy.

Prisma’s ORM ships a query engine, historically a Rust binary that gets bundled with your app or accessed via Data Proxy or Accelerate. The team has been steadily moving toward a TypeScript-native Prisma client and it’s improved a lot, but on Cloudflare Workers and similar edge runtimes the binary path is still the dominant story for many setups. That means longer cold starts and a noticeable bundle hit.

Drizzle is just TypeScript. No binary, no separate process. On the same Hono API I measured a roughly 4x cold-start improvement and a bundle smaller by tens of kilobytes when I replaced Prisma plus Accelerate with Drizzle plus postgres.js. Your mileage will vary, but the direction is consistent across every benchmark I’ve seen people post on the Drizzle GitHub discussions.

The flip side: Drizzle gives you nothing for connection pooling. You wire it up yourself with postgres, node-postgres, neon-http, @planetscale/database, whatever fits your runtime. Prisma at least pretends to manage connections for you. For a serverless app where I’m already using a pooler at the database level, that’s a wash. For a long-running Node service, Prisma’s behavior is slightly more turn-key, though the difference is small if you’ve ever configured a pool before.

I wrote up the Postgres side of this in my notes on jsonb defaults, because the ORM choice and the database choice keep colliding in ways I didn’t expect when I started.

What about JSON, RLS, and the awkward bits

A few specific areas where the two diverge in ways that matter to me.

JSON columns: Drizzle’s jsonb type takes a generic that types the contents, which is what I want most of the time. Prisma still returns Prisma.JsonValue, and you cast or validate at the boundary. Neither is wrong; Drizzle’s is what I reach for.

Row-level security: if you’re leaning on Postgres RLS the way I do for multi-tenant apps, you want fine control over the session settings. Both ORMs can do it, but with Drizzle I’m closer to the wire, which makes things like set_config('app.user_id', ...) simpler to thread through. Prisma’s session-variable story improved in recent releases, though it still feels like a workaround.

Studio and seeding: Prisma Studio is genuinely good. Drizzle Studio has caught up faster than I expected but still has rough edges around enum editing and large tables. For seed scripts I prefer Drizzle (it’s just TS calling db.insert), but prisma db seed is more standardized if your team needs a convention.

Logging: Drizzle’s logger interface gives me the SQL with parameters out of the box, which is what I want during incidents. Prisma’s event.query is decent but the format is noisier. This sounds like a small thing until you’re tailing logs on a Friday afternoon.

If you’re doing serious work, the kind I cover in my portfolio of full-stack projects, these awkward bits add up faster than you’d guess.

So which one do I actually reach for now?

Drizzle by default, especially for anything serverless or edge-deployed. The combination of plain TypeScript schemas, SQL-first migrations, and a tiny runtime is the closest thing to ‘ORM without regret’ I’ve used.

Prisma still wins for two cases I keep hitting:

  1. A team that doesn’t read SQL comfortably. Prisma’s DSL plus the nested query API plus Studio is the gentlest on-ramp from ‘I know JavaScript’ to ‘I’m shipping a database-backed app’. I’ve onboarded juniors with Drizzle and they get there, but it takes longer.

  2. Apps that depend on Prisma-specific tooling. Accelerate for caching, Pulse for change streams, a Studio-driven workflow your PM uses every day. That ecosystem is real, and pulling out of it is more painful than the ORM itself.

If you’re on the fence: pick a small read path in your current app, write it in both, then deploy both versions and watch the latency for a week. The right answer will show up faster than any blog post can tell you. Then come back and yell at me on my contact page if I got it wrong.