Pernahkah kamu ngalamin error type error runtime karena ORM framework kamu nge-generate query yang gak sesuai ekspektasi? Atau kamu harus nunggu compile time baru tau kalau ada kolom yang typo? Saya juga pernah. Dulu pakai Prisma, bukan karena gak bagus, tapi karena aku butuh sesuatu yang lebih dekat ke SQL. Begitu ketemu Drizzle ORM, rasanya kayak balik ke SQL tapi tetep dapat type safety.
Drizzle ORM itu headless TypeScript ORM yang beda dari yang lain. Dia gak punya proxy layer, gak ada magic, gak ada custom query language. Yang ada cuma SQL murni yang di-wrap dengan TypeScript types. Kamu bisa liat query SQL yang dihasilkan, kamu bisa predict behavior-nya, dan yang paling penting types nya derive langsung dari schema definition.
Apa Itu Drizzle ORM dan Kenapa Pakai?
Drizzle ORM dibuat untuk developer yang udah muak dengan ORM yang "terlalu pintar". ORM tradisional kayak GORM (Go), Eloquent (PHP), atau Prisma (TypeScript) sering bikin kamu jauh dari SQL aslinya. Drizzle ambil pendekatan berbeda dia kayak knex atau query builder tapi dengan type safety penuh.
Beberapa alasan kenapa aku makin sering pakai Drizzle di project production:
- Type safety end-to-end: Schema definition otomatis jadi TypeScript types. Gak perlu generate client kayak Prisma.
- SQL-first: Query builder-nya kayak nulis SQL. Kalau kamu bisa SQL, kamu bisa Drizzle.
- Zero dependencies: Bundle size-nya kecil banget. Gak ada runtime overhead.
- Edge-ready: Bisa jalan di Cloudflare Workers, Vercel Edge, Bun, Deno. Prisma belum support ini dengan baik.
- Migrations yang masuk akal: Generate migration SQL yang bisa kamu review sebelum apply.
Setup Project Drizzle ORM dengan PostgreSQL
Mari kita bikin project sederhana. Pertama, install dependencies yang dibutuhkan:
# Bikin project baru
mkdir drizzle-demo && cd drizzle-demo
npm init -y
# Install Drizzle + pg driver
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg typescript tsx
# Install dotenv untuk env
npm install dotenv
Setelah itu, bikin file tsconfig.json standar:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
Mendefinisikan Schema
Di Drizzle, schema di-definisikan dengan function yang mirip SQL DDL. Ini contoh schema untuk aplikasi blog sederhana:
// src/schema.ts
import { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
bio: text('bio'),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
published: boolean('published').default(false),
authorId: integer('author_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
Perhatiin gimana setiap kolom punya tipe yang explicit. serial untuk auto-increment, text untuk string, timestamp untuk datetime. Kamu bisa liat ini persis kayak DDL SQL. Foreign key juga gampang tinggal panggil .references(() => users.id).
Koneksi Database dan Query
Sekarang bikin file untuk koneksi dan mulai query:
// src/db.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
import 'dotenv/config';
const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
});
export const db = drizzle(pool, { schema });
Query insert sederhana:
// src/insert.ts
import { db } from './db';
import { users } from './schema';
async function main() {
// Insert satu user
const [newUser] = await db.insert(users).values({
email: '[email protected]',
name: 'Budi Santoso',
bio: 'Backend developer dari Bandung',
}).returning();
console.log('User baru:', newUser);
// TypeScript udah tau kalau newUser punya property id, email, name, dll
}
main();
Notice gimana .returning() langsung kasih kamu typed object. Gak perlu cast atau interface manual. TypeScript udah tau dari schema definition kalau hasilnya punya field id, email, name, bio, isActive, dan createdAt.
Query Select dengan Filter dan Join
Ini bagian yang bikin aku jatuh cinta sama Drizzle. Query builder-nya kayak nulis SQL tapi dengan autocomplete:
import { eq, and, desc, like, sql } from 'drizzle-orm';
import { db } from './db';
import { users, posts } from './schema';
// Select sederhana dengan filter
const activeUsers = await db.select().from(users)
.where(eq(users.isActive, true));
// Select dengan join
const postsWithAuthors = await db
.select({
postId: posts.id,
title: posts.title,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(10);
// Query kompleks dengan multiple conditions
const searchResults = await db
.select({
id: posts.id,
title: posts.title,
authorName: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(and(
eq(posts.published, true),
like(posts.title, '%typescript%'),
))
.orderBy(desc(posts.createdAt));
Kalau kamu pernah pakai Prisma, kamu mungkin berpikir "ini lebih ribet". Tapi coba pikirin di Drizzle kamu liat SQL-nya. Kamu tau kalau ini bikin INNER JOIN, kamu tau WHERE-nya, kamu tau ORDER BY-nya. Di Prisma, semua itu abstraksi dan kadang menghasilkan query yang gak optimal.
Update dan Delete
Update dan delete juga straightforward:
// Update user
const [updated] = await db.update(users)
.set({ bio: 'Sekarang udah jadi fullstack developer' })
.where(eq(users.id, 1))
.returning();
// Delete post yang belum published
await db.delete(posts)
.where(eq(posts.published, false));
// Soft delete pattern (pakai column is_deleted)
await db.update(posts)
.set({ published: false })
.where(eq(posts.authorId, 5));
Transaction dan Batch Operations
Transaction di Drizzle gampang banget. Cocok untuk operasi yang butuh atomicity, kayak transfer saldo atau create order dengan multiple items:
import { db } from './db';
import { users, posts } from './schema';
async function createUserWithPost(userData: { email: string; name: string }) {
return await db.transaction(async (tx) => {
// Insert user
const [user] = await tx.insert(users)
.values({
email: userData.email,
name: userData.name,
})
.returning();
// Insert post default untuk user baru
const [post] = await tx.insert(posts)
.values({
title: 'Hello World - Post Pertama Saya',
content: 'Ini post pertama saya di platform ini!',
authorId: user.id,
published: true,
})
.returning();
return { user, post };
});
}
// Kalau ada error di tengah transaction, semua rollback otomatis
Generate Migrations dengan Drizzle Kit
Drizzle Kit itu CLI tool untuk generate migration SQL dari schema definition kamu. Pertama, bikin file config:
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Generate migration pertama kamu:
npx drizzle-kit generate
# Output: ./drizzle/0000_initial_migration.sql
# File ini berisi CREATE TABLE statements
Apply migration ke database:
npx drizzle-kit migrate
# Atau push langsung (skip migration files, langsung sync schema)
npx drizzle-kit push
Yang aku suka dari migration Drizzle adalah file SQL-nya bisa kamu baca dan edit sebelum apply. Kalau ada yang gak sesuai, kamu bisa tweak manual. Beda sama Prisma yang generate SQL yang kadang susah dibaca.
Drizzle Studio GUI untuk Lihat Data
Drizzle punya GUI sendiri buat liat dan edit data, namanya Drizzle Studio:
npx drizzle-kit studio
# Buka browser di http://localhost:4983
Interface-nya sederhana tapi cukup untuk inspect data, run query, dan debug. Untuk development sehari-hari, ini lebih dari cukup. Gak perlu install pgAdmin atau DBeaver kalau cuma mau liat data cepet.
Relasi yang Lebih Kompleks
Drizzle support relasi one-to-many dan many-to-many lewat relations() function. Ini memungkinkan query nested kayak Prisma's include:
import { relations } from 'drizzle-orm';
// Definisikan relasi
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
// Query dengan relasi
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
limit: 5,
},
},
});
Hasilnya, usersWithPosts bakal punya array posts di tiap user object. TypeScript tau tipenya dengan benar karena derive dari relasi yang kamu define.
Drizzle vs Prisma Perbandingan Praktis
Buat kamu yang masih ragu, ini perbandingan dari pengalaman saya pakai keduanya di project production:
- Bundle size: Drizzle ~12KB, Prisma ~2.3MB (client + engine). Kalau deploy ke edge function, beda ini sangat keliatan.
- Type generation: Prisma butuh
prisma generatetiap kali ubah schema. Drizzle gak perlu types derive langsung dari schema file. - Query transparency: Drizzle kasih kamu SQL. Prisma generate query di belakang layar, kadang bikin N+1 tanpa kamu sadar.
- Edge support: Drizzle jalan di Cloudflare Workers dan Bun. Prisma butuh adapter khusus dan masih experimental.
- Learning curve: Drizzle lebih steep kalau kamu gak fluent SQL. Tapi kalau kamu udah bisa SQL, Drizzle terasa natural.
Untuk project yang butuh performance tinggi, edge deployment, atau control penuh atas SQL, Drizzle menang. Untuk rapid prototyping atau team yang gak terlalu concern dengan SQL, Prisma masih pilihan yang valid.
Best Practices yang Saya Pakai
Beberapa hal yang aku pelajin dari pakai Drizzle di production:
1. Pisahkan schema per entitas. Jangan taruh semua table di satu file. Bikin folder src/schema/ dengan satu file per entitas:
src/schema/
users.ts
posts.ts
comments.ts
index.ts # re-export semua
2. Pakai $withAuth pattern untuk query yang butuh user context. Bikin helper function yang inject user ID ke query:
function withUserContext(userId: number) {
return {
getMyPosts: () => db.select().from(posts)
.where(eq(posts.authorId, userId)),
createPost: (data: { title: string; content: string }) =>
db.insert(posts).values({ ...data, authorId: userId }).returning(),
};
}
3. Selalu pakai .returning() saat insert/update. Ini kasih kamu typed result tanpa query kedua. Hemat satu round-trip ke database.
4. Index kolom yang sering di-where. Drizzle gak otomatis bikin index. Kamu harus explicit:
import { index } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
// ... columns
}, (table) => ({
authorIdx: index('posts_author_id_idx').on(table.authorId),
publishedIdx: index('posts_published_idx').on(table.published),
}));
Kesimpulan
Drizzle ORM cocok banget buat developer yang sayang sama SQL tapi mau TypeScript types sebagai safety net. Setup-nya gampang, query builder-nya transparent, dan bundle size-nya kecil. Kalau kamu lagi bikin project dengan PostgreSQL, MySQL, atau SQLite dan pakai TypeScript cobain Drizzle. Saya jamin kamu bakal kaget seberapa gampang dan cepet development jadi.
Oh ya, Drizzle juga support SQLite dan MySQL, gak cuma PostgreSQL. Jadi kalau kamu pakai Turso, PlanetScale, atau SQLite lokal, tetap bisa pakai Drizzle dengan syntax yang mirip.
Kamu udah pernah cobain Drizzle belum? Atau masih setia sama Prisma? Bagikan pengalaman kamu di kolom komentar, aku penasaran apa yang bikin kamu milih salah satu.