Drizzle ORM Essentials
Drizzle ORM is a TypeScript-first database toolkit that provides type-safe database access with minimal overhead. When paired with Cloudflare D1, it creates a powerful, developer-friendly stack for building edge applications.
Why Drizzle for D1?
Drizzle wasn’t just adapted for edge environments—it was designed for them. Here’s why it’s the ideal companion for D1:
Lightweight Bundle Size
Traditional ORMs can add megabytes to your bundle. Drizzle keeps things minimal:
| ORM | Bundle Size | Edge Compatible |
|---|---|---|
| Drizzle | ~50 KB | ✅ Yes |
| Prisma | ~2+ MB | ⚠️ Limited |
| TypeORM | ~500+ KB | ❌ No |
| Sequelize | ~400+ KB | ❌ No |
Cloudflare Workers have strict size limits (1 MB for free tier, 10 MB for paid). Drizzle fits comfortably within these constraints.
First-Class D1 Support
Drizzle provides native D1 integration with zero configuration:
import { drizzle } from 'drizzle-orm/d1';
// That's it! Just pass the D1 bindingconst db = drizzle(env.DB);No connection pools, no connection strings, no driver installation. The D1 binding handles everything.
SQL-First Philosophy
Unlike ORMs that hide SQL behind abstractions, Drizzle embraces SQL:
// Drizzle query reads like SQLconst results = await db .select() .from(users) .where(eq(users.email, 'alice@example.com')) .leftJoin(posts, eq(posts.authorId, users.id));If you know SQL, you know Drizzle. There’s no new query language to learn.
Edge-Optimized Performance
Drizzle generates efficient queries without N+1 problems:
- No lazy loading: All data fetched in a single query
- Prepared statements: Queries are optimized and cached
- Minimal overhead: Thin wrapper over raw SQL
- Zero runtime dependencies: No heavy reflection or metadata
Drizzle Workflow
Understanding how Drizzle works helps you use it effectively:
flowchart LR
subgraph Development["Development"]
Schema["schema.ts<br/>(TypeScript)"]
DrizzleKit["Drizzle Kit<br/>CLI"]
Migrations["SQL Migrations<br/>(.sql files)"]
end
subgraph Runtime["Runtime"]
AppCode["Application<br/>Code"]
DrizzleORM["Drizzle ORM"]
D1["D1 Database"]
end
Schema -->|"generate"| DrizzleKit
DrizzleKit -->|"creates"| Migrations
Migrations -->|"applied to"| D1
Schema -->|"imports"| AppCode
AppCode -->|"uses"| DrizzleORM
DrizzleORM -->|"queries"| D1
The Development Flow
- Define your schema in TypeScript (
schema.ts) - Run Drizzle Kit to generate SQL migrations
- Apply migrations to your D1 database
- Import the schema in your application code
- Query with type safety using Drizzle ORM
This flow ensures your TypeScript code and database structure are always in sync.
Schema Definition Basics
Drizzle schemas are just TypeScript—no decorators, no magic strings, no separate schema files:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
// Define a users tableexport const users = sqliteTable('users', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), email: text('email').notNull().unique(), createdAt: integer('created_at', { mode: 'timestamp' }) .notNull() .$defaultFn(() => new Date()),});
// Define a posts table with a foreign keyexport const posts = sqliteTable('posts', { id: integer('id').primaryKey({ autoIncrement: true }), title: text('title').notNull(), content: text('content').notNull(), authorId: integer('author_id') .notNull() .references(() => users.id), createdAt: integer('created_at', { mode: 'timestamp' }) .notNull() .$defaultFn(() => new Date()),});Column Types for SQLite/D1
D1 uses SQLite, which has a simplified type system:
| Drizzle Type | SQLite Storage | Use Case |
|---|---|---|
integer() | INTEGER | Numbers, IDs, booleans, timestamps |
text() | TEXT | Strings, JSON, enums |
real() | REAL | Floating-point numbers |
blob() | BLOB | Binary data |
Common Column Modifiers
// Primary key with auto-incrementid: integer('id').primaryKey({ autoIncrement: true })
// Required fieldname: text('name').notNull()
// Unique constraintemail: text('email').notNull().unique()
// Default valuestatus: text('status').default('pending')
// Default function (runs at insert time)createdAt: integer('created_at', { mode: 'timestamp' }) .$defaultFn(() => new Date())
// Foreign key referenceauthorId: integer('author_id').references(() => users.id)Type Safety Benefits
Type safety is Drizzle’s superpower. Your TypeScript compiler catches database errors before your code runs.
Compile-Time Query Validation
// ✅ TypeScript knows 'email' exists on usersconst result = await db .select({ email: users.email }) .from(users);
// ❌ TypeScript error: 'emails' doesn't existconst result = await db .select({ email: users.emails }) // Error! .from(users);Inferred Result Types
Every query returns properly typed results:
// TypeScript automatically infers this type:// { id: number; name: string; email: string; createdAt: Date }[]const allUsers = await db.select().from(users);
// Partial selects are also typed:// { name: string; email: string }[]const nameAndEmail = await db .select({ name: users.name, email: users.email }) .from(users);Insert Type Checking
Drizzle ensures you provide all required fields:
// ✅ All required fields providedawait db.insert(users).values({ name: 'Alice', email: 'alice@example.com',});
// ❌ TypeScript error: 'email' is requiredawait db.insert(users).values({ name: 'Bob', // Missing email!});Relationship Type Safety
Even joins maintain full type safety:
// TypeScript knows the exact shape of joined resultsconst postsWithAuthors = await db .select({ postTitle: posts.title, authorName: users.name, }) .from(posts) .innerJoin(users, eq(posts.authorId, users.id));
// Result type: { postTitle: string; authorName: string }[]Drizzle vs Raw SQL
You might wonder: why use Drizzle instead of raw SQL queries?
Raw SQL Approach
// Raw SQL: no type safety, string-basedconst result = await env.DB.prepare( 'SELECT * FROM users WHERE email = ?').bind('alice@example.com').all();
// What's in result.results? 🤷// TypeScript doesn't know the shapeDrizzle Approach
// Drizzle: fully typed, IDE autocompleteconst result = await db .select() .from(users) .where(eq(users.email, 'alice@example.com'));
// TypeScript knows: User[]// IDE shows all available columnsBest of Both Worlds
Need raw SQL for complex queries? Drizzle supports that too:
import { sql } from 'drizzle-orm';
// Raw SQL when you need it, still type-safe resultsconst result = await db.run( sql`SELECT COUNT(*) as count FROM ${users}`);Summary
Drizzle ORM is purpose-built for modern edge development:
- Lightweight: Tiny bundle size, perfect for Workers
- Type-safe: Catch errors at compile time, not runtime
- SQL-first: Familiar syntax, no abstraction tax
- D1-native: Zero-config integration with Cloudflare D1
With the fundamentals covered, you’re ready to define your first schema and see Drizzle in action.