· development · 13 min read
Drizzle ORM — the TypeScript ORM that thinks in SQL
Drizzle ORM has quietly become one of the most popular TypeScript ORMs. Here's a comprehensive guide — from quickstart to advanced patterns, real code examples, and an honest comparison with Prisma, TypeORM, Kysely, and MikroORM.

Why another ORM?
For years, the TypeScript ORM conversation went like this: “Just use Prisma.” And for many projects, that was the right answer — Prisma offered a polished developer experience, visual tooling, and a schema-first workflow that lowered the barrier to working with databases.
But Prisma made a trade-off. Its dedicated .prisma schema language, code generation step, and heavyweight Rust-based query engine (~8 MB binary) created friction in serverless environments, edge runtimes, and projects where developers wanted to stay close to SQL. The abstraction was beautiful — until you needed a complex JOIN, a raw subquery, or a bundle under 1 MB.
Drizzle ORM takes the opposite approach. It’s a TypeScript-native ORM where schemas are plain TypeScript, queries read like SQL, and the entire library ships at ~7.4 KB gzipped with zero runtime dependencies. No code generation, no binary engine, no separate schema language. If you know SQL and TypeScript, you already know Drizzle.
Created by the Drizzle Team and first published in 2022, the project has grown to over 34,000 GitHub stars and nearly 2 million weekly npm downloads — making it the fastest-growing ORM in the TypeScript ecosystem.
Quickstart — zero to queries in five minutes
Let’s set up Drizzle with PostgreSQL from scratch. The same patterns apply to MySQL and SQLite — you’d just swap the driver and column type imports.
1. Install dependencies
npm install drizzle-orm postgres
npm install -D drizzle-kit typescript tsx| Package | Purpose |
|---|---|
drizzle-orm | Core ORM library |
postgres | postgres.js — fast PostgreSQL driver |
drizzle-kit | CLI for migrations and studio |
tsx | TypeScript execution for scripts |
You can swap postgres for pg (node-postgres), @neondatabase/serverless, or any other supported driver.
2. Define your schema
Create src/db/schema.ts:
import { pgTable, serial, text, varchar, boolean, timestamp, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 256 }).notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 256 }).notNull(),
content: text('content').notNull(),
published: boolean('published').default(false).notNull(),
authorId: integer('author_id')
.notNull()
.references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));This is the key insight: your schema is just TypeScript. No .prisma files, no decorators, no code generation. Drizzle infers all TypeScript types directly from these table definitions — the schema is the type system.
3. Configure Drizzle Kit
Create drizzle.config.ts in your project root:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema.ts',
out: './drizzle',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});4. Generate and run migrations
npx drizzle-kit generate
npx drizzle-kit migrategenerate diffs your TypeScript schema against previously generated migrations and produces SQL files. migrate applies them to your database. No shadow database, no migration server — just SQL files you can read, review, and commit.
For rapid prototyping, you can skip migration files entirely and push schema changes directly:
npx drizzle-kit push5. Connect and query
Create src/db/index.ts:
import { drizzle } from 'drizzle-orm/postgres-js';
import * as schema from './schema';
export const db = drizzle(process.env.DATABASE_URL!, { schema });That’s it — one line to create a fully typed database client. If you need more control over the underlying connection (pool size, SSL, etc.), you can pass your own driver instance:
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!, { max: 10 });
export const db = drizzle({ client, schema });Either way, you’re ready to query.
CRUD operations — SQL you can type-check
Drizzle’s query builder mirrors SQL syntax almost 1:1. If you can write a SELECT, you can write a Drizzle query.
Select
import { eq, desc, like, and, gt, sql } from 'drizzle-orm';
import { users, posts } from './db/schema';
import { db } from './db';
const allUsers = await db.select().from(users);
const specificColumns = await db
.select({ id: users.id, name: users.name })
.from(users);
const filtered = await db
.select()
.from(users)
.where(like(users.email, '%@gmail.com'));
const withJoin = await db
.select({
userName: users.name,
postTitle: posts.title,
})
.from(users)
.innerJoin(posts, eq(users.id, posts.authorId))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(10);Every query is fully typed. allUsers is inferred as { id: number; name: string; email: string; createdAt: Date }[]. No manual type annotations, no as casts.
Insert
const newUser = await db
.insert(users)
.values({
name: 'Alice',
email: '[email protected]',
})
.returning();
const batchInsert = await db
.insert(posts)
.values([
{ title: 'First post', content: 'Hello world', authorId: newUser[0].id },
{ title: 'Second post', content: 'More content', authorId: newUser[0].id },
])
.returning();Update
const updated = await db
.update(users)
.set({ name: 'Alice Smith' })
.where(eq(users.id, 1))
.returning({ id: users.id, name: users.name });Delete
const deleted = await db
.delete(posts)
.where(
and(
eq(posts.published, false),
gt(posts.createdAt, new Date('2025-01-01')),
),
)
.returning();Upsert (insert on conflict)
await db
.insert(users)
.values({ name: 'Alice', email: '[email protected]' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'Alice Updated' },
});Relational queries — the Prisma-like API
The SQL-like query builder is Drizzle’s core. But for fetching nested, related data, Drizzle also offers a relational query API that feels similar to Prisma’s include — with full type safety:
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
});
const detailed = await db.query.users.findMany({
columns: {
id: true,
name: true,
},
with: {
posts: {
where: (posts, { eq }) => eq(posts.published, true),
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
limit: 5,
columns: {
title: true,
createdAt: true,
},
},
},
});
const singleUser = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, 1),
with: {
posts: true,
},
});This API requires the relations() definitions from your schema (shown in the quickstart) and the schema option when initializing the drizzle() client. Under the hood, Drizzle translates these into optimized SQL — no N+1 queries.
Transactions
Drizzle supports both regular and nested transactions:
await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ name: 'Bob', email: '[email protected]' })
.returning();
await tx
.insert(posts)
.values({
title: 'My first post',
content: 'Written inside a transaction',
authorId: user.id,
});
});If any statement inside the callback throws, the entire transaction rolls back. The tx object exposes the same API as db, so you can pass it through your service layer without changing query code.
Prepared statements
For hot paths where you’re running the same query thousands of times, prepared statements eliminate repeated SQL parsing on the database side:
const getUserById = db
.select()
.from(users)
.where(eq(users.id, sql.placeholder('id')))
.prepare('get_user_by_id');
const user = await getUserById.execute({ id: 42 });The SQL is concatenated once on the Drizzle side, and the database driver reuses the precompiled binary representation. On complex queries, this can significantly reduce latency.
Drizzle Kit — the migration toolkit
Drizzle Kit is Drizzle’s companion CLI for schema management. It’s a separate package (drizzle-kit) that reads your TypeScript schema and your drizzle.config.ts to handle migrations.
The three workflows
| Command | What it does | When to use |
|---|---|---|
drizzle-kit generate | Diffs schema → produces SQL migration files | Production-grade migration workflow |
drizzle-kit push | Applies schema changes directly to the database | Prototyping, local development |
drizzle-kit migrate | Runs pending migration files against the database | CI/CD, deployment pipelines |
Unlike Prisma, Drizzle Kit doesn’t need a shadow database to compute diffs. It compares your current TypeScript schema against the last generated migration snapshot — entirely locally.
Drizzle Studio
npx drizzle-kit studioOpens a browser-based GUI for exploring your database — view tables, filter rows, edit data, and inspect relationships. Think of it as a lightweight alternative to pgAdmin or TablePlus, built right into your development workflow.
Supported databases and drivers
Drizzle supports the three major SQL databases with drivers optimized for different runtimes:
| Database | Drivers |
|---|---|
| PostgreSQL | postgres.js, node-postgres (pg), Neon Serverless, Neon HTTP, Vercel Postgres |
| MySQL | mysql2, PlanetScale Serverless |
| SQLite | better-sqlite3, libSQL / Turso, Cloudflare D1 |
This driver flexibility is one of Drizzle’s strongest selling points for serverless and edge deployments. You can use the Neon HTTP driver in a Cloudflare Worker, libSQL with Turso at the edge, or D1 for Cloudflare’s native SQLite — all with the same Drizzle API.
Built-in schema validation
Drizzle integrates with popular validation libraries to generate schemas directly from your table definitions:
npm install drizzle-zodimport { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { users } from './db/schema';
const insertUserSchema = createInsertSchema(users);
const selectUserSchema = createSelectSchema(users);
const validated = insertUserSchema.parse({
name: 'Alice',
email: '[email protected]',
});Supported validation libraries (each has its own companion package):
- Zod via
drizzle-zod - Valibot via
drizzle-valibot - TypeBox via
drizzle-typebox - ArkType via
drizzle-arktype
Drizzle v1 will consolidate these directly into the core drizzle-orm package (e.g., drizzle-orm/zod), but for now the separate packages are the stable approach.
This eliminates the common duplication problem where you define a schema in your ORM and a separate validation schema for your API layer. With Drizzle, they’re derived from the same source of truth.
Why Drizzle — the benefits
SQL-native mental model
Drizzle doesn’t invent its own query language. If you know SQL, you already know how to query with Drizzle. This means Stack Overflow answers, database documentation, and SQL tutorials all translate directly into Drizzle code. There’s no “how do I do X in Drizzle?” — there’s just “how do I do X in SQL?” and then you type it.
True zero-dependency, minimal footprint
At ~7.4 KB gzipped, Drizzle is orders of magnitude smaller than Prisma’s query engine. No Rust binary to download, no WASM runtime, no native modules. This matters enormously for:
- Serverless functions — faster cold starts
- Edge runtimes — Cloudflare Workers, Vercel Edge Functions, Deno Deploy
- Monorepos —
node_modulesdoesn’t balloon with per-package engines
No code generation step
Change your schema, and TypeScript knows immediately. No prisma generate, no waiting for code to regenerate, no stale types after a schema change you forgot to regenerate. Your editor’s autocomplete is always in sync because the types are derived at the TypeScript compiler level, not from generated files.
Full type inference from schema to query result
Every select, insert, update, and delete returns precisely typed results inferred from your schema. Rename a column? TypeScript errors light up everywhere it’s used. Add a notNull() constraint? The insert type now requires that field. This is type safety that works with you, not type safety that requires a build step to stay current.
First-class edge and serverless support
Drizzle works natively in environments where traditional ORMs struggle. Pair it with Neon’s serverless driver, Turso’s libSQL, or Cloudflare D1, and you have a production-grade database layer that deploys to the edge without workarounds.
The competitor landscape
Prisma — the industry standard
Prisma (~3.8 million weekly npm downloads) remains the most widely adopted TypeScript ORM. Its .prisma schema language, Prisma Studio, and auto-generated client provide the most polished developer experience in the ecosystem.
Choose Prisma over Drizzle when:
- Your team is less familiar with SQL and prefers higher-level abstractions
- You value visual tooling (Prisma Studio, Prisma Data Platform)
- You’re building a traditional server-side application where bundle size doesn’t matter
- You want the largest community and most third-party integrations
Choose Drizzle over Prisma when:
- You’re deploying to serverless or edge runtimes
- You need complex queries (JOINs, subqueries, CTEs) without falling back to raw SQL
- You want zero code generation in your build pipeline
- You prefer staying close to SQL rather than learning an abstraction layer
TypeORM — the veteran
TypeORM (~3.2 million weekly downloads) uses decorators and supports both Active Record and Data Mapper patterns. It’s been around since 2016 and has the most mature feature set — but development activity has slowed considerably.
Choose TypeORM when:
- You’re maintaining an existing TypeORM codebase
- You need decorator-based entity definitions (familiar to Java/C# developers)
Avoid TypeORM for new projects — its TypeScript inference is weaker than both Drizzle and Prisma, the decorator API adds reflect-metadata complexity, and the maintenance cadence doesn’t inspire confidence for long-term investment.
Kysely — the pure query builder
Kysely (~550K weekly downloads) is not a full ORM — it’s a type-safe SQL query builder. It doesn’t define schemas or manage migrations; instead, it introspects your existing database to generate types.
Choose Kysely over Drizzle when:
- You have an existing database with a complex schema you don’t want to redefine
- You want zero magic — just a type-safe layer over raw SQL
- You don’t need schema management or migrations from your library
Choose Drizzle over Kysely when:
- You want schema definition, migrations, and queries in one tool
- You need the relational query API for nested data fetching
- You want a more complete ORM experience, not just a query builder
MikroORM — the DDD choice
MikroORM (~1.1 million weekly downloads) implements the Unit of Work and Identity Map patterns — concepts familiar from Java’s Hibernate or .NET’s Entity Framework. It’s the most architecturally sophisticated option.
Choose MikroORM over Drizzle when:
- You’re building a domain-driven design application with complex entity graphs
- You need Unit of Work for batched, transactional persistence
- You require MongoDB support alongside SQL databases
When to use Drizzle
Drizzle is an excellent choice for:
- New TypeScript projects that want type-safe database access without ceremony
- Serverless and edge deployments where bundle size and cold start time matter
- Teams with SQL knowledge who don’t want an abstraction layer hiding their queries
- API backends (REST, tRPC, GraphQL) that need efficient, type-safe data access
- Full-stack frameworks — Drizzle integrates cleanly with Next.js, Nuxt, SvelteKit, Astro, and others
- Microservices where each service manages its own schema and migrations
When not to use Drizzle
Be honest about the trade-offs:
- Your team doesn’t know SQL — Drizzle’s SQL-first API is its strength, but it becomes a weakness if your team thinks in objects and relations rather than tables and joins. Prisma’s abstractions will be more productive for SQL-unfamiliar developers.
- You need a mature, battle-tested ecosystem — Prisma has more third-party integrations, more tutorials, more Stack Overflow answers, and a longer track record in production. Drizzle is catching up fast, but the ecosystem gap is real.
- You want maximum abstraction — if you prefer
user.posts.create()overdb.insert(posts).values(...), Drizzle’s philosophy won’t appeal to you. That’s fine — it’s a design choice, not a flaw. - MongoDB or NoSQL — Drizzle is SQL-only. If you need MongoDB support, look at Prisma or MikroORM.
- Existing large codebase on another ORM — migrating to Drizzle from Prisma or TypeORM is non-trivial. Unless you’re hitting real pain points (bundle size, query complexity, serverless limitations), the migration cost may not be worth it.
A real-world example — blog API with Drizzle and Hono
Here’s a more complete example showing Drizzle in a realistic setup with Hono, a lightweight web framework:
import { Hono } from 'hono';
import { eq } from 'drizzle-orm';
import { db } from './db';
import { users, posts } from './db/schema';
import { createInsertSchema } from 'drizzle-zod';
const app = new Hono();
const insertPostSchema = createInsertSchema(posts).omit({
id: true,
createdAt: true,
});
app.get('/users/:id/posts', async (c) => {
const userId = Number(c.req.param('id'));
const result = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, userId),
columns: { id: true, name: true },
with: {
posts: {
where: (posts, { eq }) => eq(posts.published, true),
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
},
},
});
if (!result) return c.json({ error: 'User not found' }, 404);
return c.json(result);
});
app.post('/posts', async (c) => {
const body = await c.req.json();
const parsed = insertPostSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: parsed.error.flatten() }, 400);
}
const [post] = await db.insert(posts).values(parsed.data).returning();
return c.json(post, 201);
});
app.patch('/posts/:id/publish', async (c) => {
const postId = Number(c.req.param('id'));
const [updated] = await db
.update(posts)
.set({ published: true })
.where(eq(posts.id, postId))
.returning();
if (!updated) return c.json({ error: 'Post not found' }, 404);
return c.json(updated);
});
export default app;Type safety flows from the schema through the queries to the API responses. The Zod schema for validation is derived from the same table definition. No manual type synchronization.
The bottom line
Drizzle ORM represents a philosophical shift in the TypeScript ORM space: instead of hiding SQL behind layers of abstraction, it embraces SQL and wraps it in TypeScript’s type system. The result is an ORM that’s tiny, fast, edge-ready, and surprisingly productive once you internalize its SQL-first approach.
It’s not the right choice for every project. But for TypeScript developers who think in SQL and deploy to modern infrastructure, Drizzle is the ORM that gets out of your way and lets you write the queries you actually want.