Mastering Drizzle ORM: A TypeScript-First Database Toolkit
In today’s fast-paced development environment, working with databases efficiently is crucial. Whether you’re building traditional web applications or AI-powered agents that need persistent memory, a reliable database layer is essential. Enter Drizzle ORM, a lightweight, type-safe, and developer-friendly ORM for TypeScript that’s gaining popularity for its simplicity and performance. If you want your AI agents to have memory across sessions or maintain state, you’ll need a database solution like Drizzle to store and retrieve that information efficiently. This blog post will explore what Drizzle is, how to use it, and demonstrate a complete end-to-end example.
What is Drizzle?
Drizzle ORM is a TypeScript-first ORM (Object-Relational Mapping) library designed to provide a type-safe interface for interacting with relational databases. Unlike traditional ORMs that often rely on decorators, classes, or runtime magic, Drizzle focuses on:
- Type Safety: Leveraging TypeScript’s type system to catch errors at compile time
- Performance: Minimal runtime overhead compared to other ORMs
- Developer Experience: Intuitive API that feels natural to TypeScript developers
- SQL-like Syntax: Query builder that closely resembles SQL, making it easier to understand
Drizzle supports multiple database engines including PostgreSQL, MySQL, SQLite, and more. It’s particularly well-suited for projects where type safety and performance are priorities.
Drizzle Architecture
flowchart TD
A[TypeScript Application] --> B[Drizzle ORM]
B --> C[Database Driver]
C --> D[(Database)]
subgraph "Drizzle Components"
E[Schema Definition] --> B
F[Query Builder] --> B
G[Migration Tools] --> B
end
How to Use Drizzle
Let’s walk through the basic steps of using Drizzle ORM in a TypeScript project.
1. Installation
First, you’ll need to install Drizzle ORM and the appropriate database driver. For PostgreSQL:
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg
2. Define Your Schema
One of Drizzle’s strengths is its declarative schema definition. Here’s how you define a simple users table:
// src/db/schema.ts
import { pgTable, serial, varchar, integer, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 50 }).notNull(),
age: integer("age").notNull(),
createdAt: timestamp("created_at").defaultNow(),
});
This code defines a PostgreSQL table named “users” with id, name, age, and createdAt columns. The schema is type-safe, meaning TypeScript will catch any type errors when you interact with this table.
Schema to Database Flow
flowchart LR
A[Schema Definition] -->|drizzle-kit generate| B[SQL Migration Files]
B -->|drizzle-kit push/migrate| C[(Database)]
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#bfb,stroke:#333,stroke-width:2px
3. Set Up Database Connection
Next, create a database client:
// src/db/client.ts
import "dotenv/config";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";
const pool = new Pool({
host: process.env.PGHOST,
port: Number(process.env.PGPORT),
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
database: process.env.PGDATABASE,
});
export const db = drizzle(pool, { schema });
This code creates a PostgreSQL connection pool and initializes Drizzle with our schema.
4. Basic CRUD Operations
Now you can perform database operations with full type safety:
// src/index.ts
import { db } from "./db/client";
import { users } from "./db/schema";
import { eq } from "drizzle-orm";
async function main() {
// Insert a new user
await db.insert(users).values({ name: "Charlie", age: 28 });
// Select all users
const result = await db.select().from(users);
console.log("Users:", result);
// Update a user
await db.update(users).set({ age: 29 }).where(eq(users.name, "Charlie"));
// Delete a user
await db.delete(users).where(eq(users.name, "Charlie"));
}
main().then(() => process.exit(0));
CRUD Operations Flow
sequenceDiagram
participant App as Application
participant Drizzle as Drizzle ORM
participant DB as Database
App->>Drizzle: db.insert(users).values({...})
Drizzle->>DB: INSERT INTO users (name, age) VALUES (...)
DB-->>Drizzle: Result
Drizzle-->>App: Return result
App->>Drizzle: db.select().from(users)
Drizzle->>DB: SELECT * FROM users
DB-->>Drizzle: Result rows
Drizzle-->>App: Return typed results
App->>Drizzle: db.update(users).set({...}).where(...)
Drizzle->>DB: UPDATE users SET age = 29 WHERE name = 'Charlie'
DB-->>Drizzle: Result
Drizzle-->>App: Return result
App->>Drizzle: db.delete(users).where(...)
Drizzle->>DB: DELETE FROM users WHERE name = 'Charlie'
DB-->>Drizzle: Result
Drizzle-->>App: Return result
Wrapping Drizzle: Creating Convenient CLI Commands
One of the advantages of Drizzle is its excellent CLI tool, drizzle-kit. Let’s see how to set up common database tasks as npm scripts.
In your package.json
, add these scripts:
"scripts": {
"db:generate": "drizzle-kit generate --config=drizzle.config.ts",
"db:migrate": "drizzle-kit migrate --config=drizzle.config.ts",
"db:push": "drizzle-kit push --config=drizzle.config.ts",
"db:studio": "drizzle-kit studio --config=drizzle.config.ts",
"db:seed": "ts-node src/db/seed.ts",
"dev": "ts-node src/index.ts"
}
These scripts provide convenient shortcuts for common database operations:
- db:generate: Generates SQL migration files based on schema changes
- db:migrate: Applies migrations to your database
- db:push: Pushes schema changes directly to the database (useful during development)
- db:studio: Launches Drizzle Studio, a web UI for managing your database
- db:seed: Runs a custom script to seed your database with initial data
- dev: Runs your application
Drizzle CLI Workflow
flowchart TD
A[Schema Changes] --> B[npm run db:generate]
B --> C[Migration Files]
C --> D{Development or Production?}
D -->|Development| E[npm run db:push]
D -->|Production| F[npm run db:migrate]
E --> G[(Database Updated)]
F --> G
G --> H[npm run db:seed]
H --> I[Database with Data]
I --> J[npm run dev]
style A fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#bfb,stroke:#333,stroke-width:2px
style I fill:#bfb,stroke:#333,stroke-width:2px
You’ll also need a configuration file for drizzle-kit:
// drizzle.config.ts
import type { Config } from "drizzle-kit";
export default {
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
host: process.env.PGHOST ?? "localhost",
port: Number(process.env.PGPORT ?? 5432),
user: process.env.PGUSER ?? "postgres",
password: process.env.PGPASSWORD ?? "postgres",
database: process.env.PGDATABASE ?? "demo",
},
} satisfies Config;
End-to-End Example
Let’s put everything together with a complete example of setting up and using Drizzle ORM with PostgreSQL.
Step 1: Project Setup
First, set up your project structure:
drizzle-demo/
├── docker-compose.yml
├── drizzle.config.ts
├── package.json
├── tsconfig.json
└── src/
├── index.ts
└── db/
├── client.ts
├── schema.ts
└── seed.ts
Project Structure and Data Flow
flowchart TD
A[schema.ts] --> B[client.ts]
B --> C[seed.ts]
B --> D[index.ts]
E[drizzle.config.ts] --> F[drizzle-kit]
F --> G[(Database)]
C --> G
D --> G
style A fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#bbf,stroke:#333,stroke-width:2px
style G fill:#bfb,stroke:#333,stroke-width:2px
Step 2: Database Setup with Docker
Use Docker Compose to set up PostgreSQL:
# docker-compose.yml
version: "3.9"
services:
db:
image: postgres:16
container_name: drizzle_demo_pg
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: demo
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d demo"]
interval: 5s
timeout: 3s
retries: 10
adminer:
image: adminer:4
container_name: drizzle_demo_adminer
restart: unless-stopped
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:
Start the database:
docker-compose up -d
Step 3: Create a Seed Script
To populate your database with initial data:
// src/db/seed.ts
import { db } from "./client";
import { users } from "./schema";
async function seed() {
await db.insert(users).values([
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
]);
console.log("✅ Seed data inserted");
}
seed().then(() => process.exit(0));
Step 4: Run Your Application
Now you can run your application:
- Start the database:
docker-compose up -d
- Generate migrations:
npm run db:generate
- Apply migrations:
npm run db:push
- Seed the database:
npm run db:seed
- Run the application:
npm run dev
Complete Application Workflow
sequenceDiagram
participant Dev as Developer
participant Docker as Docker
participant Drizzle as Drizzle Tools
participant App as Application
participant DB as Database
Dev->>Docker: docker-compose up -d
Docker->>DB: Start PostgreSQL
Dev->>Drizzle: npm run db:generate
Drizzle->>Drizzle: Create migration files
Dev->>Drizzle: npm run db:push
Drizzle->>DB: Apply schema changes
Dev->>App: npm run db:seed
App->>DB: Insert initial data
Dev->>App: npm run dev
App->>DB: Perform CRUD operations
DB-->>App: Return results
App-->>Dev: Display output
Conclusion
Drizzle ORM offers a refreshing approach to database access in TypeScript applications. Its focus on type safety, performance, and developer experience makes it an excellent choice for modern applications.
Key benefits include:
- Type Safety: Catch errors at compile time rather than runtime
- Performance: Minimal overhead compared to traditional ORMs
- Developer Experience: Intuitive API that feels natural to TypeScript developers
- Flexibility: Works with multiple database engines
- Tooling: Excellent CLI tools for migrations and database management
Whether you’re building a small project or a large application, Drizzle ORM provides a solid foundation for your database access layer. Give it a try and experience the benefits of a TypeScript-first ORM!
Sample Code Repository
For a complete working example of the code discussed in this blog post, check out the reference implementation at my github.
This repository contains all the files and configurations needed to get started with Drizzle ORM and PostgreSQL.
Drizzle vs Traditional ORMs
When comparing Drizzle to traditional ORMs, several key differences become apparent in their approach to database interaction:
graph LR
subgraph "Traditional ORM"
A1[Class-based Models] --> B1[Runtime Type Checking]
B1 --> C1[Heavy Abstractions]
C1 --> D1[SQL Generation]
D1 --> E1[(Database)]
end
subgraph "Drizzle ORM"
A2[Schema Definitions] --> B2[Compile-time Type Checking]
B2 --> C2[Lightweight Abstractions]
C2 --> D2[SQL Generation]
D2 --> E2[(Database)]
end
style A2 fill:#bfb,stroke:#333,stroke-width:2px
style B2 fill:#bfb,stroke:#333,stroke-width:2px
style C2 fill:#bfb,stroke:#333,stroke-width:2px
Traditional ORMs typically use class-based models with decorators and rely on runtime type checking, which can lead to errors being discovered only during execution. They often introduce heavy abstractions that hide SQL details, which can make debugging and performance optimization challenging.
In contrast, Drizzle takes a more lightweight approach:
-
Schema Definitions vs Class Models: Drizzle uses plain object definitions for schemas rather than classes with decorators, resulting in cleaner, more maintainable code.
-
Compile-time vs Runtime Type Checking: By leveraging TypeScript’s type system, Drizzle catches type errors during development rather than at runtime, improving reliability.
-
Lightweight vs Heavy Abstractions: Drizzle provides a thinner layer over SQL, giving developers more control and visibility into the generated queries while still offering type safety.
-
SQL-like Syntax: Drizzle’s query builder closely resembles SQL, making it more intuitive for developers familiar with SQL and easier to predict what SQL will be generated.
-
Performance: With fewer abstractions and runtime overhead, Drizzle typically offers better performance compared to heavier ORMs.
This approach makes Drizzle particularly well-suited for TypeScript projects where type safety, performance, and developer experience are priorities. It strikes a balance between the raw power of SQL and the convenience of an ORM, without sacrificing the benefits of either.