Skip to main content

Why I Switched from Prisma to Drizzle (and Almost Switched Back)

·PkgPulse Team

TL;DR

Drizzle is the right choice if you think in SQL and value control. Prisma is the right choice if you want a managed experience and are willing to pay the price. After migrating a mid-sized production app, Drizzle's bundle size and edge runtime support are genuinely better. But Prisma's migrations, relational queries, and onboarding experience are genuinely better too. The "almost switched back" moment came when writing complex relational queries in Drizzle — it's powerful but verbose. Here's the full picture.

Key Takeaways

  • Drizzle wins on: bundle size (5KB vs 40KB+), edge compatibility, SQL transparency
  • Prisma wins on: migrations, include for relations, Prisma Studio, onboarding
  • The migration: schema translation was easy; query translation took 2 days
  • Production result: Drizzle performs similarly; Drizzle debugging is harder
  • The honest recommendation: Prisma for teams, Drizzle for SQL-comfortable developers

Why I Switched

Context:
→ Mid-sized Next.js app: 15 tables, ~50 API routes, deployed on Vercel
→ Had been on Prisma for 2 years
→ Problem 1: Vercel Edge Functions
    → Prisma Client uses a query engine binary
    → Binary doesn't work in V8 isolates (Cloudflare Workers, Edge functions)
    → Had to carve out edge routes to avoid Prisma
    → Prisma Accelerate solves this but adds another managed service + cost

→ Problem 2: Bundle size
    → Prisma client: 40-100KB+ in the browser (when accidentally included)
    → Even server-side, the generated client is large
    → Drizzle: ~5KB

→ Problem 3: The "magic" bothered me
    → Prisma generates queries I can't see
    → When performance issues happened, I had to learn Prisma's IR
    → Couldn't easily add raw SQL optimizations

→ Why Drizzle:
    → "Drizzle is SQL with TypeScript types"
    → Works in edge environments
    → Tiny bundle
    → You write SQL (basically)

The Migration: What Was Easy

// Schema translation: straightforward

// Prisma schema:
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
}

// Drizzle schema (drizzle/schema.ts):
import { pgTable, text, boolean, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

export const users = pgTable('users', {
  id:        text('id').primaryKey().$defaultFn(() => createId()),
  email:     text('email').notNull().unique(),
  name:      text('name'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

export const posts = pgTable('posts', {
  id:        text('id').primaryKey().$defaultFn(() => createId()),
  title:     text('title').notNull(),
  content:   text('content'),
  published: boolean('published').default(false).notNull(),
  authorId:  text('author_id').notNull().references(() => users.id),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

// Relations (separate from schema):
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));

// Verdict: schema migration was mechanical. Took 3 hours for 15 tables.

The Migration: What Was Hard

// Query translation: where the 2 days went

// Prisma (simple and readable):
const users = await prisma.user.findMany({
  where: {
    posts: {
      some: {
        published: true,
        createdAt: { gte: thirtyDaysAgo },
      },
    },
  },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      take: 5,
    },
    _count: { select: { posts: true } },
  },
  orderBy: { createdAt: 'desc' },
  take: 20,
});

// Drizzle (equivalent query):
const users = await db.query.users.findMany({
  where: (users, { exists, and, gte }) =>
    exists(
      db.select().from(posts).where(
        and(
          eq(posts.authorId, users.id),
          eq(posts.published, true),
          gte(posts.createdAt, thirtyDaysAgo),
        )
      )
    ),
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: [desc(posts.createdAt)],
      limit: 5,
    },
  },
  orderBy: [desc(users.createdAt)],
  limit: 20,
});
// Note: Drizzle doesn't have _count equivalent in relational API
// Need to add a separate count query or use SQL directly

// Verdict: Drizzle's query API is more verbose for complex relational queries.
// "It's SQL" is both the feature and the drawback.
// I miss Prisma's include + _count syntax for complex aggregations.

Where Drizzle Is Genuinely Better

// 1. Edge runtime compatibility
// middleware.ts (Next.js Edge)
import { db } from './db';
import { users } from './schema';
import { eq } from 'drizzle-orm';

export async function middleware(req: NextRequest) {
  // This works in Vercel Edge, Cloudflare Workers, Deno Deploy:
  const user = await db.select().from(users)
    .where(eq(users.id, req.headers.get('x-user-id')!))
    .limit(1);

  if (!user) return NextResponse.redirect('/login');
  return NextResponse.next();
}
// Prisma: this would fail (query engine binary, no edge support)

// 2. Raw SQL when you need it (seamless):
const result = await db.execute(
  sql`
    SELECT u.id, u.name, COUNT(p.id) as post_count,
           AVG(p.views) as avg_views
    FROM users u
    LEFT JOIN posts p ON p.author_id = u.id
    WHERE u.created_at > ${thirtyDaysAgo}
    GROUP BY u.id, u.name
    HAVING COUNT(p.id) > 5
    ORDER BY avg_views DESC
    LIMIT 20
  `
);
// Prisma: raw SQL exists but feels like escape hatch
// Drizzle: raw SQL and typed queries are first-class citizens

// 3. Bundle size
// Server bundle analysis:
// Prisma client: 40-120KB (plus the binary engine)
// Drizzle + pg driver: ~15KB total

// For serverless with short cold starts: this matters

Where Prisma Is Still Better

// 1. Migrations are excellent
// prisma migrate dev --name add-user-role
// → Generates SQL migration
// → Applies it
// → Updates the Prisma Client
// → One command, fully managed

// Drizzle migrations:
// npx drizzle-kit generate
// npx drizzle-kit migrate
// → Generates SQL files
// → You manage them
// → Fine, but more manual
// → No equivalent of Prisma's migration history/rollback tooling

// 2. Prisma Studio
// npx prisma studio
// → Opens web UI to browse/edit your database
// → Incredibly useful for debugging, data exploration, seeding
// Drizzle: no equivalent (third-party tools exist but aren't integrated)

// 3. Aggregations and counts are easier
// Prisma:
const result = await prisma.post.groupBy({
  by: ['authorId'],
  _count: { id: true },
  _sum: { views: true },
  having: { id: { _count: { gt: 5 } } },
});

// Drizzle (equivalent):
const result = await db
  .select({
    authorId: posts.authorId,
    count: count(posts.id),
    totalViews: sum(posts.views),
  })
  .from(posts)
  .groupBy(posts.authorId)
  .having(gt(count(posts.id), 5));
// Both work. Prisma is just more ergonomic for this pattern.

// 4. The onboarding experience
// Prisma: schema.prisma → generate → start querying
// Drizzle: schema.ts → relations setup → query builders → more learning
// Teams new to ORMs find Prisma dramatically easier to learn

After 18 Months: The Verdict

Would I switch back?
Honestly: if I were starting today, I'd choose Drizzle for solo/small-team projects.
For a larger team or a product-focused startup: Prisma.

The real factors that determined my choice:
1. My app runs on Vercel Edge → Drizzle wins on this alone
2. I'm comfortable with SQL → Drizzle's verbosity doesn't bother me
3. Solo developer → I can live without Prisma Studio

If I had:
→ A team of 5+ developers → Prisma (easier onboarding, clearer patterns)
→ Mostly CRUD operations → Prisma (its API is better for this)
→ Complex analytics/reporting → Drizzle (raw SQL feels natural)
→ Any edge function requirement → Drizzle (clear winner)
→ Traditional server deployment → either works

The "almost switched back" moment:
Writing a report query with 4 joins, 3 aggregations, conditional filtering.
Prisma would be 10 lines. Drizzle was 35 lines.
The SQL was correct but verbose.
I kept Drizzle because of edge requirements, but I understood the tradeoff.

The meta-lesson:
ORM debates are as much about team composition and app requirements
as they are about technical merit.
Both Prisma and Drizzle are excellent.
The right answer depends on your context.

Compare Prisma vs Drizzle download trends, bundle size, and health scores at PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.