Skip to content

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:

ORMBundle SizeEdge 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 binding
const 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 SQL
const 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

  1. Define your schema in TypeScript (schema.ts)
  2. Run Drizzle Kit to generate SQL migrations
  3. Apply migrations to your D1 database
  4. Import the schema in your application code
  5. 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 table
export 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 key
export 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 TypeSQLite StorageUse Case
integer()INTEGERNumbers, IDs, booleans, timestamps
text()TEXTStrings, JSON, enums
real()REALFloating-point numbers
blob()BLOBBinary data

Common Column Modifiers

// Primary key with auto-increment
id: integer('id').primaryKey({ autoIncrement: true })
// Required field
name: text('name').notNull()
// Unique constraint
email: text('email').notNull().unique()
// Default value
status: text('status').default('pending')
// Default function (runs at insert time)
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date())
// Foreign key reference
authorId: 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 users
const result = await db
.select({ email: users.email })
.from(users);
// ❌ TypeScript error: 'emails' doesn't exist
const 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 provided
await db.insert(users).values({
name: 'Alice',
email: 'alice@example.com',
});
// ❌ TypeScript error: 'email' is required
await db.insert(users).values({
name: 'Bob',
// Missing email!
});

Relationship Type Safety

Even joins maintain full type safety:

// TypeScript knows the exact shape of joined results
const 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-based
const 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 shape

Drizzle Approach

// Drizzle: fully typed, IDE autocomplete
const result = await db
.select()
.from(users)
.where(eq(users.email, 'alice@example.com'));
// TypeScript knows: User[]
// IDE shows all available columns

Best 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 results
const 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.