Drizzle ORM Complete Guide: TypeScript Database Toolkit 2026
Master Drizzle ORM 2026: schema definition, migrations, relations, RLS support, and Drizzle Studio. See how it compares to Prisma for TypeScript projects.
Drizzle ORM has become the go-to TypeScript database toolkit for developers who want type safety without sacrificing SQL control. In 2026, itβs the default choice for new Cloudflare Workers, Neon, and PlanetScale projects β and for good reason. This guide covers everything you need to go from zero to production.
Why Drizzle?
Before Drizzle, TypeScript developers faced a hard choice: use Prisma for type safety and pay the performance/bundle size tax, or drop down to raw SQL and lose type inference. Drizzle solves this by making your schema the source of truth and generating types at compile time, not runtime.
Key design principles:
- SQL-first: Drizzleβs query API mirrors SQL closely β if you know SQL, Drizzle feels natural
- Zero runtime overhead: No reflection, no proxy objects, just compiled TypeScript
- Single source of truth: Your schema file defines both the database structure and TypeScript types
- Serverless-ready: Works with Cloudflare D1, Turso, Neon, PlanetScale out of the box
Installation and Setup
npm install drizzle-orm
npm install -D drizzle-kit
# For PostgreSQL
npm install postgres
# For MySQL
npm install mysql2
# For SQLite (or Turso/Cloudflare D1)
npm install better-sqlite3
Connecting to PostgreSQL
// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });
For Neon serverless:
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);
Schema Definition
Drizzle schemas are plain TypeScript β no decorators, no magic. You define tables using column builders:
// src/db/schema.ts
import {
pgTable,
serial,
text,
varchar,
integer,
boolean,
timestamp,
uuid,
jsonb,
} from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: text('name').notNull(),
role: text('role', { enum: ['admin', 'user', 'moderator'] }).default('user'),
metadata: jsonb('metadata').$type<{ preferences: Record<string, string> }>(),
emailVerified: boolean('email_verified').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
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'),
published: boolean('published').default(false),
authorId: uuid('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
publishedAt: timestamp('published_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Drizzle infers types automatically
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
MySQL Schema
import { mysqlTable, int, varchar, boolean, datetime } from 'drizzle-orm/mysql-core';
export const products = mysqlTable('products', {
id: int('id').primaryKey().autoincrement(),
name: varchar('name', { length: 255 }).notNull(),
price: int('price').notNull(), // store in cents
active: boolean('active').default(true),
createdAt: datetime('created_at').notNull(),
});
Querying
Drizzleβs query builder closely mirrors SQL while maintaining full type safety.
Basic CRUD
import { db } from './db';
import { users, posts } from './db/schema';
import { eq, and, or, like, gte, lte, desc, asc } from 'drizzle-orm';
// SELECT
const allUsers = await db.select().from(users);
// SELECT with WHERE
const adminUsers = await db
.select()
.from(users)
.where(eq(users.role, 'admin'));
// SELECT specific columns
const userNames = await db
.select({ id: users.id, email: users.email })
.from(users);
// INSERT
const [newUser] = await db
.insert(users)
.values({
email: '[email protected]',
name: 'Alice Johnson',
})
.returning();
// INSERT multiple
await db.insert(users).values([
{ email: '[email protected]', name: 'Bob' },
{ email: '[email protected]', name: 'Carol' },
]);
// UPDATE
await db
.update(users)
.set({ emailVerified: true, updatedAt: new Date() })
.where(eq(users.id, userId));
// DELETE
await db.delete(posts).where(eq(posts.id, postId));
Complex Queries
// JOINs
const postsWithAuthors = await db
.select({
postId: posts.id,
title: posts.title,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.publishedAt))
.limit(10);
// Compound WHERE
const recentPosts = await db
.select()
.from(posts)
.where(
and(
eq(posts.published, true),
gte(posts.publishedAt, new Date('2026-01-01')),
or(
like(posts.title, '%TypeScript%'),
like(posts.title, '%JavaScript%')
)
)
);
Relational Queries
Drizzleβs query API (built on top of the core API) provides Prisma-style include and with syntax:
// Define relations in schema
import { relations } from 'drizzle-orm';
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
// Use relational API
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
where: eq(posts.published, true),
orderBy: [desc(posts.createdAt)],
limit: 5,
},
},
where: eq(users.emailVerified, true),
});
// Type: Array<User & { posts: Post[] }>
Migrations
Drizzle Kit handles migrations. Add a config file:
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;
# Generate migration files from schema changes
npx drizzle-kit generate
# Apply migrations to database
npx drizzle-kit migrate
# Push schema directly (dev only β no migration files)
npx drizzle-kit push
# Open Drizzle Studio
npx drizzle-kit studio
Generated migration files are plain SQL β no magic, easy to review:
-- drizzle/migrations/0001_add_email_verified.sql
ALTER TABLE "users" ADD COLUMN "email_verified" boolean DEFAULT false;
Row Level Security (RLS) Support
Drizzle 0.30+ adds first-class RLS support for PostgreSQL, critical for multi-tenant apps:
import { pgTable, pgPolicy, authenticatedRole } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
export const documents = pgTable(
'documents',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
content: text('content'),
},
(table) => [
pgPolicy('users_own_documents', {
as: 'permissive',
for: 'all',
to: authenticatedRole,
using: sql`${table.userId} = auth.uid()`,
}),
]
);
This integrates cleanly with Supabase Row Level Security policies.
Drizzle Studio
Drizzle Studio is a database browser that runs locally against your schema:
npx drizzle-kit studio
# Opens at https://local.drizzle.studio
It gives you:
- Table browser with filtering and sorting
- Query editor with schema-aware autocomplete
- Relation graph visualization
- Read/write data directly (with confirmation for writes)
Performance vs. Prisma
The most common migration path in 2026 is Prisma β Drizzle. Key differences:
| Aspect | Drizzle | Prisma |
|---|---|---|
| Bundle size | ~35 KB | ~2.5 MB (query engine) |
| Cold start | ~5ms | ~200-400ms |
| Query generation | Compile-time | Runtime (Rust engine) |
| Schema syntax | TypeScript | Prisma SDL |
| Raw SQL | db.execute(sql\β¦`)` | $queryRaw |
| Serverless support | Excellent | Improved (Accelerate) |
| Migrations | Drizzle Kit (SQL) | Prisma Migrate |
| ORM style | SQL-first | Object-first |
For Cloudflare Workers and edge functions where cold starts are measured in milliseconds, Drizzleβs startup time is a decisive advantage.
Benchmark: Simple query on Neon Serverless
| p50 | p99 | |
|---|---|---|
| Drizzle (neon-http) | 12ms | 28ms |
| Prisma (Accelerate) | 18ms | 45ms |
| Raw pg | 11ms | 25ms |
PostgreSQL, MySQL, and SQLite Support
Drizzle has feature parity across all three:
// SQLite / Turso / Cloudflare D1
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
const client = createClient({ url: process.env.TURSO_URL!, authToken: process.env.TURSO_TOKEN! });
export const db = drizzle(client);
// Cloudflare D1
import { drizzle } from 'drizzle-orm/d1';
export function createDb(d1: D1Database) {
return drizzle(d1);
}
Transactions
await db.transaction(async (tx) => {
const [order] = await tx
.insert(orders)
.values({ userId, total })
.returning();
await tx.insert(orderItems).values(
items.map(item => ({ orderId: order.id, ...item }))
);
await tx
.update(inventory)
.set({ stock: sql`${inventory.stock} - 1` })
.where(inArray(inventory.productId, items.map(i => i.productId)));
});
Best Practices
1. Export inferred types from your schema:
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
2. Create a db.ts singleton per environment:
// Don't create a new connection on every request
let _db: ReturnType<typeof drizzle> | null = null;
export function getDb() {
if (!_db) _db = drizzle(client, { schema });
return _db;
}
3. Use prepare() for hot-path queries:
const getUserById = db
.select()
.from(users)
.where(eq(users.id, sql.placeholder('id')))
.prepare('get_user_by_id');
// Reuse the prepared statement
const user = await getUserById.execute({ id: userId });
Getting Started
Drizzle ORM is the right choice if you:
- Deploy to serverless or edge environments
- Want SQL-level control with TypeScript types
- Need fast cold starts
- Work with multiple databases in the same project
Start with drizzle-kit push during development, graduate to migration files before production, and use Drizzle Studio for data exploration throughout.
Resources
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.