Web Development

Drizzle ORM: TypeScript ORM Modern dan Cepat

Tutorial lengkap belajar Drizzle ORM — schema definition, queries, migrations, relasi, dan best practices untuk aplikasi TypeScript

1. Pengenalan Drizzle ORM

Drizzle ORM adalah ORM (Object-Relational Mapping) ringan dan modern yang dirancang khusus untuk TypeScript. Berbeda dari ORM tradisional seperti Prisma atau TypeORM, Drizzle mengambil pendekatan yang sangat berbeda — ia menawarkan API yang sangat mendekati SQL asli namun tetap mendapatkan keuntungan penuh dari type safety TypeScript.

Drizzle ORM dibangun dengan filosofi "SQL-like" yang artinya query yang kamu tulis akan terasa seperti menulis SQL biasa, tetapi dengan keamanan tipe (type safety) dari TypeScript yang kuat. Ini menjadikan Drizzle pilihan yang sangat baik untuk developer yang menginginkan kontrol penuh atas query tanpa mengorbankan fitur ORM.

Mengapa Drizzle ORM?

Fitur Drizzle ORM Prisma TypeORM
Pendekatan SQL-like, code-first Schema-first Decorator-based
Performance ⚡ Sangat cepat Sedang Sedang
Type Safety ⭐⭐⭐ Sangat kuat ⭐⭐⭐ Sangat kuat ⭐⭐ Cukup
Bundle Size ~30KB ~5MB (engine) ~2MB
Database Support PostgreSQL, MySQL, SQLite Banyak Banyak
Learning Curve Rendah (jika tahu SQL) Rendah Sedang
💡 Tips

Jika kamu sudah familiar dengan SQL, Drizzle ORM akan terasa sangat natural karena API-nya dirancang menyerupai sintaks SQL yang sebenarnya.

Fitur Utama Drizzle ORM

2. Instalasi & Setup

Mari kita mulai dengan menginstal Drizzle ORM dan menyiapkan proyek TypeScript pertama. Kita akan menggunakan PostgreSQL sebagai database, tetapi kamu bisa menggantinya dengan MySQL atau SQLite sesuai kebutuhan.

Proyek Baru dengan Drizzle

Bash
# Buat direktori proyek
mkdir belajar-drizzle
cd belajar-drizzle

# Inisialisasi proyek Node.js
npm init -y

# Instal TypeScript dan dependencies
npm install -D typescript @types/node tsx

# Instal Drizzle ORM dan driver PostgreSQL
npm install drizzle-orm pg

# Instal Drizzle Kit untuk CLI & migrasi
npm install -D drizzle-kit

# Inisialisasi TypeScript
npx tsc --init

Koneksi Database

Buat file src/db/index.ts untuk mengatur koneksi ke database PostgreSQL:

TypeScript
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';

// Buat connection pool ke PostgreSQL
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
});

// Inisialisasi Drizzle ORM dengan schema
export const db = drizzle(pool, { schema });

Drizzle Config

Buat file drizzle.config.ts di root proyek:

TypeScript
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  verbose: true,
  strict: true,
});
⚠️ Peringatan

Jangan pernah hardcode database URL di kode. Selalu gunakan environment variable DATABASE_URL untuk keamanan.

3. Definisi Schema

Schema di Drizzle ORM didefinisikan langsung menggunakan TypeScript. Ini berbeda dari Prisma yang menggunakan bahasa schema terpisah (Prisma Schema Language). Kelebihannya adalah tidak perlu code generation — kamu langsung mendapatkan type safety dari definisi schema yang ada.

Definisi Tabel Dasar

Buat file src/db/schema.ts:

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

// Enum untuk status pengguna
export const userStatusEnum = pgEnum('user_status', [
  'active',
  'inactive',
  'banned',
]);

// Definisi tabel users
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  passwordHash: text('password_hash').notNull(),
  age: integer('age'),
  isActive: boolean('is_active').default(true).notNull(),
  status: userStatusEnum('status').default('active').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

Tipe Data di Drizzle

Drizzle Type SQL Type Contoh
serial SERIAL / Auto-increment serial('id').primaryKey()
varchar VARCHAR(n) varchar('name', { length: 255 })
text TEXT text('content')
integer INTEGER integer('count')
boolean BOOLEAN boolean('is_active')
timestamp TIMESTAMP timestamp('created_at')
real REAL / FLOAT real('price')
jsonb JSONB jsonb('metadata')
uuid UUID uuid('id').defaultRandom()
pgEnum ENUM pgEnum('status', [...])

Tabel dengan Kolom Tambahan

TypeScript
// Definisi tabel posts
export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: varchar('title', { length: 500 }).notNull(),
  slug: varchar('slug', { length: 500 }).notNull().unique(),
  content: text('content'),
  excerpt: text('excerpt'),
  isPublished: boolean('is_published').default(false).notNull(),
  viewCount: integer('view_count').default(0).notNull(),
  authorId: integer('author_id').notNull()
    .references(() => users.id, { onDelete: 'cascade' }),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

// Definisi tabel tags
export const tags = pgTable('tags', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 100 }).notNull().unique(),
  slug: varchar('slug', { length: 100 }).notNull().unique(),
});

Indeks dan Constraints

TypeScript
import { pgTable, serial, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core';

export const products = pgTable('products', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  sku: varchar('sku', { length: 50 }).notNull(),
  category: varchar('category', { length: 100 }),
  price: integer('price').notNull(),
}, (table) => {
  return {
    // Single column index
    nameIdx: index('products_name_idx').on(table.name),
    // Unique index untuk SKU
    skuIdx: uniqueIndex('products_sku_idx').on(table.sku),
    // Composite index
    categoryPriceIdx: index('products_cat_price_idx')
      .on(table.category, table.price),
  };
});

4. CRUD Queries

Drizzle ORM menawarkan dua cara untuk melakukan query: Query Builder (SQL-like) dan Relational Queries (seperti Prisma). Pada bagian ini kita akan fokus pada Query Builder yang merupakan pendekatan utama Drizzle.

Select — Membaca Data

TypeScript
import { db } from './db';
import { users, posts } from './db/schema';
import { eq, and, or, like, gte, lte, desc, sql } from 'drizzle-orm';

// SELECT * FROM users
const allUsers = await db.select().from(users);

// SELECT id, name, email FROM users
const partialUsers = await db
  .select({
    id: users.id,
    name: users.name,
    email: users.email,
  })
  .from(users);

// SELECT * FROM users WHERE id = 1
const user = await db
  .select()
  .from(users)
  .where(eq(users.id, 1))
  .limit(1);

// SELECT * FROM users WHERE is_active = true AND age >= 18
const activeAdults = await db
  .select()
  .from(users)
  .where(
    and(
      eq(users.isActive, true),
      gte(users.age, 18)
    )
  );

Filter dengan Operators

TypeScript
import { eq, ne, gt, gte, lt, lte, like, ilike, 
         inArray, notInArray, isNull, isNotNull,
         and, or, not, between } from 'drizzle-orm';

// WHERE name LIKE '%john%' (case insensitive)
const searchResults = await db.select().from(users)
  .where(ilike(users.name, '%john%'));

// WHERE id IN (1, 2, 3, 4)
const selectedUsers = await db.select().from(users)
  .where(inArray(users.id, [1, 2, 3, 4]));

// WHERE age BETWEEN 18 AND 30
const youngAdults = await db.select().from(users)
  .where(between(users.age, 18, 30));

// WHERE is_active = true OR status = 'banned'
const specialUsers = await db.select().from(users)
  .where(or(
    eq(users.isActive, true),
    eq(users.status, 'banned')
  ));

// Complex: WHERE (age > 18 AND is_active = true) OR status = 'banned'
const complex = await db.select().from(users)
  .where(or(
    and(gt(users.age, 18), eq(users.isActive, true)),
    eq(users.status, 'banned')
  ));

Insert — Menambahkan Data

TypeScript
// INSERT INTO users (name, email, password_hash, age)
// VALUES ('Budi', 'budi@email.com', 'hashed_pw', 25)
const newUser = await db.insert(users).values({
  name: 'Budi',
  email: 'budi@email.com',
  passwordHash: 'hashed_password_here',
  age: 25,
}).returning();

// returning() mengembalikan baris yang baru di-insert
console.log(newUser[0].id); // ID dari user yang baru dibuat

// Insert multiple rows sekaligus
const batchInsert = await db.insert(users).values([
  { name: 'Andi', email: 'andi@mail.com', passwordHash: 'pw1', age: 22 },
  { name: 'Sari', email: 'sari@mail.com', passwordHash: 'pw2', age: 28 },
  { name: 'Dewi', email: 'dewi@mail.com', passwordHash: 'pw3', age: 19 },
]).returning();

// INSERT ON CONFLICT (Upsert)
const upserted = await db.insert(users).values({
  name: 'Budi Updated',
  email: 'budi@email.com',
  passwordHash: 'new_hash',
}).onConflictDoUpdate({
  target: users.email,
  set: { name: 'Budi Updated' },
}).returning();

Update — Mengubah Data

TypeScript
// UPDATE users SET name = 'Budi Santoso' WHERE id = 1
const updated = await db.update(users)
  .set({
    name: 'Budi Santoso',
    updatedAt: new Date(),
  })
  .where(eq(users.id, 1))
  .returning();

// Update dengan raw SQL expression
const viewIncrement = await db.update(posts)
  .set({
    viewCount: sql`${posts.viewCount} + 1`,
  })
  .where(eq(posts.id, 1))
  .returning();

// Update semua yang belum publish
await db.update(posts)
  .set({ isPublished: true })
  .where(eq(posts.isPublished, false));

Delete — Menghapus Data

TypeScript
// DELETE FROM users WHERE id = 1
const deleted = await db.delete(users)
  .where(eq(users.id, 1))
  .returning();

// DELETE dengan kondisi kompleks
await db.delete(users)
  .where(
    and(
      eq(users.isActive, false),
      lt(users.createdAt, new Date('2025-01-01'))
    )
  );

Sorting & Pagination

TypeScript
import { desc, asc } from 'drizzle-orm';

// ORDER BY created_at DESC LIMIT 10
const recentPosts = await db.select()
  .from(posts)
  .orderBy(desc(posts.createdAt))
  .limit(10);

// Pagination: LIMIT dan OFFSET
const page = 2;
const pageSize = 10;
const paginated = await db.select()
  .from(posts)
  .orderBy(desc(posts.createdAt))
  .limit(pageSize)
  .offset((page - 1) * pageSize);

// ORDER BY dengan multiple kolom
const sorted = await db.select()
  .from(posts)
  .orderBy(
    desc(posts.isPublished),
    desc(posts.viewCount),
    asc(posts.title)
  );

Aggregation & Grouping

TypeScript
import { count, sum, avg, min, max } from 'drizzle-orm';

// COUNT(*) FROM users
const totalUsers = await db
  .select({ count: count() })
  .from(users);

// GROUP BY status
const statusCounts = await db
  .select({
    status: users.status,
    count: count(),
  })
  .from(users)
  .groupBy(users.status);

// HAVING: filter setelah grouping
const activeStatuses = await db
  .select({
    status: users.status,
    count: count(),
  })
  .from(users)
  .groupBy(users.status)
  .having(gt(count(), 5));

// Aggregation functions
const stats = await db
  .select({
    totalPosts: count(posts.id),
    totalViews: sum(posts.viewCount),
    avgViews: avg(posts.viewCount),
    maxViews: max(posts.viewCount),
  })
  .from(posts);

5. Relasi Antar Tabel

Drizzle mendefinisikan relasi menggunakan relations(). Relasi tidak menambah kolom baru ke tabel, melainkan mendefinisikan cara tabel-tabel saling berhubungan. Kamu bisa mendefinisikan relasi one-to-one, one-to-many, dan many-to-many.

One-to-Many (User → Posts)

TypeScript
import { relations } from 'drizzle-orm';

// User memiliki banyak posts
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

// Post dimiliki oleh satu user
export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

Many-to-Many (Posts ↔ Tags)

TypeScript
// Tabel junction untuk relasi many-to-many
export const postsToTags = pgTable('posts_to_tags', {
  postId: integer('post_id').notNull()
    .references(() => posts.id, { onDelete: 'cascade' }),
  tagId: integer('tag_id').notNull()
    .references(() => tags.id, { onDelete: 'cascade' }),
});

// Relasi
export const postsToTagsRelations = relations(postsToTags, ({ one }) => ({
  post: one(posts, {
    fields: [postsToTags.postId],
    references: [posts.id],
  }),
  tag: one(tags, {
    fields: [postsToTags.tagId],
    references: [tags.id],
  }),
}));

export const postsRelationsUpdated = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
  postsToTags: many(postsToTags),
}));

export const tagsRelations = relations(tags, ({ many }) => ({
  postsToTags: many(postsToTags),
}));

Relational Query API

Setelah mendefinisikan relasi, kamu bisa menggunakan db.query untuk melakukan nested queries:

TypeScript
// Ambil user beserta semua posts-nya
const userWithPosts = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: {
    posts: true,
  },
});
// Hasil: { id: 1, name: 'Budi', posts: [{ id: 1, title: '...', ... }] }

// Nested relations: user → posts → tags
const userFull = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: {
    posts: {
      with: {
        postsToTags: {
          with: {
            tag: true,
          },
        },
      },
      where: eq(posts.isPublished, true),
    },
  },
});

// findMany dengan filter
const activeUsersWithPosts = await db.query.users.findMany({
  where: eq(users.isActive, true),
  with: {
    posts: {
      limit: 5,
      orderBy: desc(posts.createdAt),
    },
  },
});

6. Migrasi Database

Drizzle Kit menyediakan tools untuk mengelola migrasi database secara terstruktur. Kamu memiliki dua pilihan: migrate (file-based migrations) atau push (langsung apply ke database).

Generate Migrasi

Bash
# Generate file migrasi SQL dari perubahan schema
npx drizzle-kit generate

# Output: ./drizzle/0000_initial.sql
# File berisi CREATE TABLE, ALTER TABLE, dll.

# Push langsung ke database (untuk development)
npx drizzle-kit push

# Jalankan migrasi ke database
npx drizzle-kit migrate

# Lihat perbedaan antara schema dan database
npx drizzle-kit diff

# Drop semua tabel (hati-hati!)
npx drizzle-kit drop

Cara Migrasi Bekerja

Ketika kamu mengubah schema, Drizzle Kit akan mendeteksi perbedaan dan membuat file SQL migrasi. File ini bisa di-commit ke version control dan dijalankan secara otomatis di production:

SQL
-- drizzle/0000_initial.sql
CREATE TABLE IF NOT EXISTS "users" (
  "id" serial PRIMARY KEY NOT NULL,
  "name" varchar(255) NOT NULL,
  "email" varchar(255) NOT NULL,
  "password_hash" text NOT NULL,
  "age" integer,
  "is_active" boolean DEFAULT true NOT NULL,
  "status" user_status DEFAULT 'active' NOT NULL,
  "created_at" timestamp DEFAULT now() NOT NULL,
  "updated_at" timestamp DEFAULT now() NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS "users_email_unique"
  ON "users" ("email");

CREATE TYPE "user_status" AS ENUM ('active', 'inactive', 'banned');

Menjalankan Migrasi di Kode

TypeScript
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { db } from './db';

// Jalankan semua file migrasi di folder ./drizzle
async function runMigrations() {
  console.log('Menjalankan migrasi...');
  await migrate(db, { migrationsFolder: './drizzle' });
  console.log('Migrasi selesai!');
}

runMigrations().catch(console.error);
💡 Tips

Gunakan drizzle-kit push untuk development (cepat & langsung) dan drizzle-kit generate + migrate untuk production (aman & terkontrol).

7. Transactions

Transactions memungkinkan kamu menjalankan beberapa operasi database secara atomik — semua berhasil atau semua gagal. Ini sangat penting untuk menjaga konsistensi data.

Basic Transaction

TypeScript
// Transaction: buat user dan post pertama sekaligus
const result = await db.transaction(async (tx) => {
  // Step 1: Buat user baru
  const newUser = await tx.insert(users).values({
    name: 'Budi',
    email: 'budi@email.com',
    passwordHash: 'hashed_pw',
    age: 25,
  }).returning();

  // Step 2: Buat post pertama untuk user tersebut
  const newPost = await tx.insert(posts).values({
    title: 'Post Pertama',
    slug: 'post-pertama',
    content: 'Hello World!',
    authorId: newUser[0].id,
  }).returning();

  // Kembalikan hasil
  return { user: newUser[0], post: newPost[0] };
});

console.log(result.user.id);
console.log(result.post.title);

Transaction dengan Savepoints

TypeScript
const result = await db.transaction(async (tx) => {
  // Operasi pertama selalu dijalankan
  const user = await tx.insert(users).values({
    name: 'Sari',
    email: 'sari@email.com',
    passwordHash: 'pw',
  }).returning();

  try {
    // Operasi yang mungkin gagal
    await tx.insert(posts).values({
      title: 'Post Sari',
      slug: 'post-sari',
      authorId: user[0].id,
    });
  } catch (error) {
    // Jika gagal, transaction tetap berjalan
    console.log('Gagal membuat post, tapi user tetap dibuat');
  }

  return user[0];
});

Isolation Levels

TypeScript
// Transaction dengan isolation level khusus
await db.transaction(async (tx) => {
  // Set isolation level untuk transaction ini
  await tx.execute(
    sql`SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`
  );

  // Operasi database yang sensitif terhadap concurrency
  const balance = await tx.select()
    .from(accounts)
    .where(eq(accounts.id, 1));

  await tx.update(accounts)
    .set({ balance: balance[0].balance - 100 })
    .where(eq(accounts.id, 1));
});

8. Drizzle Kit CLI

Drizzle Kit adalah CLI yang mendampingi Drizzle ORM untuk mengelola schema, migrasi, dan visualisasi data. Berikut adalah semua command yang tersedia:

Command Fungsi
drizzle-kit generate Generate file SQL migrasi dari perubahan schema
drizzle-kit migrate Jalankan file migrasi ke database
drizzle-kit push Push langsung schema ke database (dev only)
drizzle-kit pull Pull schema dari database yang sudah ada (introspection)
drizzle-kit studio Buka Drizzle Studio (GUI) di browser
drizzle-kit drop Hapus file migrasi
drizzle-kit diff Tampilkan perbedaan schema vs database
drizzle-kit check Periksa konsistensi file migrasi

Drizzle Studio

Drizzle Studio adalah GUI interaktif yang memungkinkan kamu melihat, mengedit, dan mengelola data di browser:

Bash
# Buka Drizzle Studio
npx drizzle-kit studio

# Output:
# Drizzle Studio is running
# URL: https://local.drizzle.studio

Introspection (Pull Schema)

Jika kamu sudah memiliki database yang sudah ada dan ingin membuat Drizzle schema darinya:

Bash
# Pull schema dari database yang sudah ada
npx drizzle-kit pull

# Ini akan generate file schema.ts otomatis
# berdasarkan struktur database yang ada

9. Best Practices

Struktur Folder

Text
src/
  db/
    index.ts          # Koneksi database
    schema/
      users.ts        # Schema tabel users
      posts.ts        # Schema tabel posts
      relations.ts    # Definisi relasi
      index.ts        # Export semua schema
  services/
    user.service.ts   # Query functions untuk users
    post.service.ts   # Query functions untuk posts
  drizzle/
    0000_initial.sql  # File migrasi
    0001_add_tags.sql
drizzle.config.ts

Separate Schema Files

TypeScript
// src/db/schema/users.ts
import { pgTable, serial, varchar, boolean, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  isActive: boolean('is_active').default(true).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

// src/db/schema/index.ts
export * from './users';
export * from './posts';
export * from './tags';
export * from './relations';

Reusable Query Functions

TypeScript
// src/services/user.service.ts
import { db } from '../db';
import { users } from '../db/schema';
import { eq, and } from 'drizzle-orm';

export const UserService = {
  async findById(id: number) {
    return db.query.users.findFirst({
      where: eq(users.id, id),
    });
  },

  async findWithPosts(id: number) {
    return db.query.users.findFirst({
      where: eq(users.id, id),
      with: { posts: true },
    });
  },

  async create(data: { name: string; email: string; passwordHash: string }) {
    const result = await db.insert(users).values(data).returning();
    return result[0];
  },

  async deactivate(id: number) {
    const result = await db.update(users)
      .set({ isActive: false })
      .where(eq(users.id, id))
      .returning();
    return result[0];
  },
};
⚠️ Peringatan

Selalu gunakan parameterized query dari Drizzle ORM dan hindari interpolasi string langsung untuk mencegah SQL injection. Drizzle sudah menangani ini secara otomatis, jadi jangan bypass fitur keamanan ini.

Tips Performance

10. Quiz Pemahaman

Uji pemahamanmu tentang Drizzle ORM dengan menjawab pertanyaan berikut:

Pertanyaan 1: Apa filosofi utama Drizzle ORM?

a) SQL-like API dengan type safety TypeScript
b) Menggunakan bahasa schema terpisah
c) Pendekatan decorator-based
d) Menggunakan GraphQL sebagai interface

Pertanyaan 2: Bagaimana cara mendefinisikan tabel di Drizzle ORM?

a) Menggunakan file .schema terpisah
b) Menggunakan fungsi pgTable() dengan TypeScript
c) Menggunakan XML configuration
d) Menggunakan JSON files

Pertanyaan 3: Apa fungsi dari .returning() di Drizzle ORM?

a) Mengembalikan koneksi ke pool
b) Mengembalikan baris yang dimodifikasi oleh query
c) Mengembalikan error message
d) Mengembalikan jumlah baris yang dihapus

Pertanyaan 4: Perbedaan antara drizzle-kit push dan drizzle-kit generate + migrate?

a) Tidak ada perbedaan, keduanya sama
b) Push langsung apply ke database (dev), generate+migrate buat file SQL terlebih dahulu (prod)
c) Push hanya untuk MySQL, generate untuk PostgreSQL
d) Push lebih aman untuk production

Pertanyaan 5: Bagaimana cara mendefinisikan relasi many-to-many di Drizzle?

a) Dengan menambahkan array foreign key di tabel
b) Dengan membuat tabel junction dan mendefinisikan relasi dengan many()
c) Tidak bisa, Drizzle tidak mendukung many-to-many
d) Dengan menggunakan keyword @manyToMany dalam schema
🔍 Zoom
100%
🎨 Tema