Migrasi Prisma → Drizzle 90 Menit
Saya migrate 4 project klien Prisma → Drizzle 2025-2026. Average 90 menit per project. Bundle 6x lebih kecil, edge-compatible. Guide step-by-step plus gotcha.
TL;DR
- Recommended migrate Prisma → Drizzle untuk project edge-first atau bundle-sensitive.
- Effort: 60-120 menit untuk schema 15-25 table (saya validate 4x).
- Gain: bundle 6x lebih kecil, cold start 5-10x lebih cepat, edge-compatible.
Konteks
Saya migrate 4 project klien Prisma → Drizzle Q4 2025 - Q2 2026:
- SaaS dental Jakarta (Next.js 15, 18 table): 95 menit migration + 3 hari testing
- Ecommerce klinik kecantikan (Astro + Hono backend, 22 table): 110 menit + 2 hari testing
- AI legal assistant (Next.js 15 + Cloudflare Workers, 12 table): 75 menit + 2 hari testing (motivation: edge compat)
- Internal dashboard HR (Next.js + Vercel, 28 table): 130 menit + 4 hari testing
Average migration: 102 menit. Average testing: 3 hari. Zero rollback. Drizzle vs Prisma vs Kysely detail.
Pricing
Prisma dan Drizzle sama-sama gratis. Migration cost = waktu dev.
- Solo dev senior (Rp 250rb/jam): 102 menit = Rp 425rb
- Testing 3 hari × 4 jam = 12 jam × Rp 250rb = Rp 3 juta
Total migration cost: Rp 3.4 juta untuk project medium.
ROI: cold start saving 300-800ms × 50K invocation/bulan = save UX delay. Plus bundle size reduction membantu edge deployment (yang tidak possible dengan Prisma).
Why migrate
1. Edge compatibility
Prisma butuh Query Engine binary native — tidak work di Cloudflare Workers atau Vercel Edge runtime tanpa Driver Adapter (beta). Drizzle native edge.
2. Bundle size
- Prisma client: 1.5MB+ (Query Engine + generated code)
- Drizzle: 250KB total
Untuk CF Workers (1MB limit untuk paid plan), Prisma struggle.
3. Cold start
- Prisma cold start: 300-800ms
- Drizzle cold start: 30-80ms
5-10x improvement. Significant untuk UX serverless.
4. Codegen overhead
Prisma butuh prisma generate setiap schema change. Forget = type out of sync. Drizzle: no codegen, type inferred from schema TS file.
5. SQL control
Drizzle syntax SQL-like. Bisa write raw SQL fragment dengan type safety. Prisma abstract SQL — harder debug complex query.
Step-by-step (90 menit average)
Step 1: Setup branch + backup (5 menit)
git checkout -b migrate/drizzle
pg_dump $DATABASE_URL > backup-pre-drizzle.sql
Step 2: Install Drizzle (5 menit)
bun add drizzle-orm pg
bun add -D drizzle-kit @types/pg
Atau untuk Neon/Supabase/Cloudflare D1:
# Neon
bun add @neondatabase/serverless
# Supabase (postgres-js)
bun add postgres
# Cloudflare D1
# (D1 driver bawaan Wrangler)
Step 3: Convert schema (30-40 menit)
Prisma schema:
model User {
id String @id @default(cuid())
email String @unique
name String
role String @default("user")
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
title String
published Boolean @default(false)
}
Drizzle equivalent (src/db/schema.ts):
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core';
import { createId } from '@paralleldrive/cuid2';
import { relations } from 'drizzle-orm';
export const users = pgTable('User', {
id: text('id').primaryKey().$defaultFn(() => createId()),
email: text('email').notNull().unique(),
name: text('name').notNull(),
role: text('role').default('user').notNull(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
});
export const posts = pgTable('Post', {
id: text('id').primaryKey().$defaultFn(() => createId()),
userId: text('userId').notNull().references(() => users.id),
title: text('title').notNull(),
published: boolean('published').default(false).notNull(),
});
// Relations untuk query relational API
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
user: one(users, { fields: [posts.userId], references: [users.id] }),
}));
Penting: gunakan pgTable('User', ...) (capitalized) match exactly table name Prisma generate. Cek _prisma_migrations atau database introspection.
Untuk schema besar (20+ table): pakai Claude Code untuk batch convert. Prompt: “Convert this Prisma schema to Drizzle, preserve table names exactly, include relations API.”
Step 4: Setup Drizzle config (5 menit)
drizzle.config.ts:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: { url: process.env.DATABASE_URL! },
});
Step 5: Baseline migration (10 menit)
# Pull current DB state ke Drizzle migration history
bunx drizzle-kit pull
# Atau jika prefer fresh start:
# Drop _prisma_migrations table di DB
# bunx drizzle-kit generate (creates initial migration from schema TS)
Step 6: Refactor query (30-45 menit)
Setup DB client (src/db/index.ts):
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
Untuk edge runtime (Cloudflare Workers + Neon):
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
Refactor query example:
| Prisma | Drizzle |
|---|---|
prisma.user.findUnique({ where: { id } }) | db.select().from(users).where(eq(users.id, id)) |
prisma.user.findUnique({ where: { id }, include: { posts: true } }) | db.query.users.findFirst({ where: eq(users.id, id), with: { posts: true } }) |
prisma.user.create({ data: {...} }) | db.insert(users).values({...}).returning() |
prisma.user.update({ where: { id }, data: {...} }) | db.update(users).set({...}).where(eq(users.id, id)) |
prisma.user.delete({ where: { id } }) | db.delete(users).where(eq(users.id, id)) |
Pakai Claude Code untuk batch refactor: “Refactor semua Prisma query di src/ menjadi Drizzle equivalent. Keep behavior identik.”
Step 7: Remove Prisma (5 menit)
bun remove prisma @prisma/client
rm -rf prisma/ # Atau preserve untuk reference
Update env: DATABASE_URL tetap sama format.
Step 8: Test (variable, 1-3 hari)
# Run typecheck
bun run typecheck
# Run unit test
bun test
# Run integration test
bun test:integration
# Manual smoke test di staging
Critical path test:
- Auth flow (signup, login, logout)
- CRUD operation untuk resource utama
- Complex query (filter, sort, paginate)
- Transaction (jika ada)
- Migration: ensure no schema drift
Performance gain (4 project)
Cold start
| Project | Prisma | Drizzle | Speedup |
|---|---|---|---|
| SaaS dental | 580ms | 75ms | 7.7x |
| Ecommerce klinik | 720ms | 95ms | 7.6x |
| AI legal | 480ms (degraded due to edge) | 45ms (native edge) | 10.7x |
| Dashboard HR | 650ms | 85ms | 7.6x |
Bundle size
| Project | Prisma client | Drizzle |
|---|---|---|
| SaaS dental | 1.6 MB | 245 KB |
| Ecommerce klinik | 1.7 MB | 280 KB |
| AI legal | 1.5 MB | 220 KB |
| Dashboard HR | 1.8 MB | 310 KB |
Consistent 6-7x bundle reduction.
Query latency
Similar (database = bottleneck, not ORM). Drizzle marginal lebih cepat untuk simple query karena no engine overhead.
Common gotcha
1. Cuid/uuid generation
Prisma @default(cuid()) generated at DB level via Prisma. Drizzle: pakai $defaultFn(() => createId()) (client-side) atau setup Postgres extension untuk uuid native.
2. DateTime field
Prisma DateTime → Drizzle timestamp({ mode: 'date' }). Watch out untuk timezone handling — explicit set ke ‘date’ mode untuk JS Date object.
3. JSON column
Prisma Json field → Drizzle jsonb('field').$type<MyType>(). Type cast required.
4. Relation include vs query API
Prisma include: { posts: true } mostly straightforward to Drizzle with: { posts: true }. Tapi nested deep include syntax beda.
5. Raw query
Prisma prisma.$queryRaw → Drizzle db.execute(sql\…`)`. Beda template literal syntax.
6. Migration history
Jangan biarkan kedua tool (Prisma + Drizzle) jalan di DB sama. Pilih satu — drop migration history tool lain.
Konteks Indonesia
SaaS dental Jakarta: migrate motivated oleh edge deploy goal. Setelah Drizzle: bisa migrate ke Cloudflare Workers. Cold start 580ms → 45ms. Customer experience noticeable improvement.
Ecommerce klinik kecantikan: migrate untuk bundle size — Astro frontend + Hono backend di CF Workers. Prisma sebelumnya force backend ke VPS. Setelah Drizzle: full edge deploy, bill turun Rp 600rb/bulan → Rp 100rb/bulan.
AI legal assistant: migrate primary motivation karena Prisma tidak work di CF Workers (Driver Adapter masih beta). Drizzle native edge — feature jalan production tanpa workaround.
Verdict
Recommended migrate Prisma → Drizzle untuk:
Migrate jika:
- Deploy di edge (CF Workers, Vercel Edge, Bun)
- Cold start sensitive untuk UX
- Bundle size matter (mobile + slow connection)
- Butuh kontrol SQL granular
- Pakai stack modern (Hono, Astro, Bun)
Stay Prisma jika:
- Project legacy stable, no active feature work
- Team familiar Prisma + butuh Prisma Studio GUI heavy
- Schema introspection auto important (Prisma menang)
- Multi-database (Mongo + Postgres) — Prisma cross-DB support lebih matang
- Deploy hanya di Node.js server (no edge)
Migration playbook:
- Validate need (audit bill, cold start metric, edge deploy goal)
- Test migration di branch + staging dulu
- Plan testing 2-4 hari sebelum cutover
- Cutover saat low-traffic window
Untuk ORM comparison lebih dalam: Drizzle vs Prisma vs Kysely. Untuk RSC pattern: Drizzle vs Kysely vs Prisma di RSC 2026.
Ditulis oleh Asti Larasati