Introduction
Drizzle ORM is a lightweight, type-safe ORM for TypeScript that provides the benefits of SQL without the overhead of heavier ORMs like Prisma. This guide covers everything you need to know to get started with Drizzle.
Understanding Drizzle
What is Drizzle?
Drizzle is a lightweight ORM that:
- Generates SQL from type-safe queries
- Has zero dependencies
- Provides full TypeScript support
- Works with PostgreSQL, MySQL, and SQLite
- Offers a small bundle size (~50KB)
// Drizzle is just SQL, but type-safe
const users = await db
.select()
.from(usersTable)
.where(eq(usersTable.name, 'John'))
Why Drizzle?
| Feature | Drizzle | Prisma |
|---|---|---|
| Bundle Size | ~50KB | ~500KB+ |
| Dependencies | Zero | Many |
| Raw SQL | Native support | Limited |
| Learning Curve | Low (know SQL) | Medium |
| Performance | Faster | Slower |
Getting Started
Installation
# Install Drizzle and database driver
npm install drizzle-orm pg
npm install -D drizzle-kit
# For MySQL
npm install drizzle-orm/mysql2
# For SQLite
npm install drizzle-orm/better-sqlite3
Configuration
// drizzle.config.ts
import type { Config } from 'drizzle-kit'
export default {
schema: './src/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config
Schema Definition
Basic Tables
// src/schema.ts
import { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
password: text('password').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
userId: integer('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
published: boolean('published').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
// Type inference
export type User = typeof users.$inferSelect
export type NewUser = typeof users.$inferInsert
Relationships
// One-to-many
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
})
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
userId: integer('user_id').references(() => users.id),
title: text('title').notNull(),
})
// Many-to-many with junction table
export const postsTags = pgTable('posts_tags', {
postId: integer('post_id').references(() => posts.id),
tagId: integer('tag_id').references(() => tags.id),
})
export const tags = pgTable('tags', {
id: serial('id').primaryKey(),
name: text('name').notNull().unique(),
})
Indexes and Constraints
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
}, (table) => {
return {
// Composite index
nameIdx: index('name_idx').on(table.name, table.email),
// Partial index (via SQL)
}
})
Queries
Select
import { db } from './db'
import { users, posts } from './schema'
import { eq, desc, like, and, or, sql } from 'drizzle-orm'
// Simple select
const allUsers = await db.select().from(users)
// Select specific columns
const userNames = await db
.select({ name: users.name, email: users.email })
.from(users)
// Where clause
const john = await db
.select()
.from(users)
.where(eq(users.name, 'John'))
// Multiple conditions
const result = await db
.select()
.from(users)
.where(
and(
eq(users.name, 'John'),
like(users.email, '%@example.com')
)
)
// Order and limit
const recentPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(10)
// Join
const userWithPosts = await db
.select({
userId: users.id,
userName: users.name,
postTitle: posts.title,
})
.from(users)
.leftJoin(posts, eq(users.id, posts.userId))
Insert
// Single insert
const [newUser] = await db
.insert(users)
.values({
name: 'John',
email: '[email protected]',
password: 'hashed',
})
.returning()
// Multiple inserts
await db.insert(users).values([
{ name: 'John', email: '[email protected]', password: 'hash' },
{ name: 'Jane', email: '[email protected]', password: 'hash' },
])
// Upsert
await db
.insert(users)
.values({ name: 'John', email: '[email protected]', password: 'hash' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'John Updated' },
})
Update
// Update single row
const [updated] = await db
.update(users)
.set({ name: 'John Updated' })
.where(eq(users.id, 1))
.returning()
// Update multiple rows
await db
.update(posts)
.set({ published: true })
.where(eq(posts.userId, 1))
Delete
// Delete
await db
.delete(users)
.where(eq(users.id, 1))
Raw SQL
Executing Raw SQL
// Raw query
const result = await db.execute(sql`
SELECT * FROM users
WHERE name = ${searchTerm}
`)
// Raw select
const users = await db
.select()
.from(sql`(SELECT * FROM users WHERE active = true) as active_users`)
Custom Types
// JSON column
const settings = pgTable('settings', {
id: serial('id').primaryKey(),
config: json('config').$type<{ theme: string; notifications: boolean }>(),
})
// Enum
const statusEnum = pgEnum('status', ['pending', 'active', 'completed'])
export const tasks = pgTable('tasks', {
id: serial('id').primaryKey(),
status: statusEnum('status').default('pending'),
})
Transactions
import { db } from './db'
import { users, posts } from './schema'
await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ name: 'John', email: '[email protected]', password: 'hash' })
.returning()
await tx.insert(posts).values({
title: 'My First Post',
userId: user.id,
})
return user
})
Drizzle with Next.js
App Router
// src/lib/db.ts
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from '@/schema'
const queryClient = postgres(process.env.DATABASE_URL!)
export const db = drizzle(queryClient, { schema })
// src/app/actions.ts
'use server'
import { db } from '@/lib/db'
import { users } from '@/lib/schema'
import { eq } from 'drizzle-orm'
export async function getUser(id: number) {
return db.select().from(users).where(eq(users.id, id)).limit(1)
}
export async function createUser(data: { name: string; email: string }) {
return db.insert(users).values(data).returning()
}
Route Handlers
// src/app/api/users/route.ts
import { db } from '@/lib/db'
import { users } from '@/lib/schema'
import { NextResponse } from 'next/server'
export async function GET() {
const allUsers = await db.select().from(users)
return NextResponse.json(allUsers)
}
Drizzle Kit (Migrations)
Generating Migrations
# Generate migration from schema
npx drizzle-kit generate:pg
# Push schema to database
npx drizzle-kit push:pg
# Pull schema from database
npx drizzle-kit pull:pg
Migration Files
-- drizzle/0010_welcome_tales.sql
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL UNIQUE,
"password" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
Drizzle vs Prisma
| Aspect | Drizzle | Prisma |
|---|---|---|
| Setup | More config | Easier setup |
| Migrations | SQL files | Schema changes |
| Type Safety | 100% | Good |
| Learning | Know SQL | New syntax |
| Performance | Faster | Slower |
| Bundle | Smaller | Larger |
Best Practices
1. Use Type Inference
// โ
Good - Use inferred types
export type User = typeof users.$inferSelect
// โ Bad - Manual types
interface User {
id: number
name: string
}
2. Schema Organization
// src/schema/index.ts
export * from './users'
export * from './posts'
export * from './comments'
3. Transaction for Multi-Step Operations
// โ
Good - Transaction ensures atomicity
await db.transaction(async (tx) => {
await tx.update(balance).set({...})
await tx.insert(transactionLog).values({...})
})
// โ Bad - Separate operations
await db.update(balance).set({...})
await db.insert(transactionLog).values({...})
Conclusion
Drizzle ORM is excellent when you:
- Want type-safe SQL without overhead
- Prefer writing SQL-like queries
- Need small bundle size
- Value performance
- Already know SQL
Perfect for: Next.js apps, serverless, performance-critical applications.
Comments