Web Development · Backend
Drizzle ORM: TypeScript-First Database Access That Gets Out of Your Way
Drizzle ORM has become the go-to choice for TypeScript projects that want type safety without the overhead of a heavy ORM. Here's how it works, how to migrate from Prisma, and when to use it.
Anurag Verma
6 min read
Sponsored
Prisma made type-safe database access mainstream in the Node.js ecosystem. But it came with trade-offs: a non-standard schema format, a heavy query engine binary, and limited flexibility when you needed to write SQL that its abstractions didn’t cover.
Drizzle ORM takes a different approach. Your schema is TypeScript. Your migrations are SQL. Your queries look close to SQL. The library stays thin: no separate binary, no generated files to check into git, no runtime code generation. You write TypeScript and get fully typed database calls.
It’s gained enough traction to be worth understanding, especially for projects where you want to run on edge runtimes like Cloudflare Workers or Vercel Edge, where Prisma’s query engine has historically caused problems.
How Drizzle Thinks About Schema
In Prisma, you define your schema in .prisma files, a custom DSL. Drizzle defines schema in TypeScript:
// src/db/schema.ts
import { pgTable, serial, text, integer, timestamp, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow().notNull(),
isActive: boolean('is_active').default(true).notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
body: text('body'),
authorId: integer('author_id').references(() => users.id).notNull(),
publishedAt: timestamp('published_at'),
});
This file is just TypeScript. Your editor understands it. You can import it, write utilities around it, and use it in tests without any special tooling. The schema types flow through into your queries automatically.
Querying
Drizzle provides two query APIs: the SQL-like query builder and a relational query API for joins.
Query builder:
import { db } from './db';
import { users, posts } from './schema';
import { eq, and, gte, desc } from 'drizzle-orm';
// Select with filter
const activeUsers = await db
.select()
.from(users)
.where(eq(users.isActive, true));
// The return type is inferred: { id: number; email: string; name: string | null; ... }[]
// Insert
const [newUser] = await db
.insert(users)
.values({ email: 'dev@example.com', name: 'Alice' })
.returning();
// Update
await db
.update(users)
.set({ name: 'Alice Smith' })
.where(eq(users.id, newUser.id));
// Delete
await db
.delete(users)
.where(and(
eq(users.isActive, false),
gte(users.createdAt, cutoffDate),
));
Relational queries (joins without writing JOIN):
For this to work, you define relations in the 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],
}),
}));
Then query with nested includes:
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
where: (posts, { isNull }) => isNull(posts.publishedAt),
columns: {
id: true,
title: true,
},
},
},
where: (users, { eq }) => eq(users.isActive, true),
});
// Type: { id: number; email: string; posts: { id: number; title: string }[] }[]
The return type is precisely typed based on what you included. If you only select id and title, you won’t get body in the result type.
Migrations With Drizzle Kit
Schema changes generate SQL migrations rather than running them automatically. This is intentional: you can review the migration before running it.
# Install Drizzle Kit (dev dependency)
npm install -D drizzle-kit
# Generate migration from schema changes
npx drizzle-kit generate
# Apply migrations
npx drizzle-kit migrate
The generated migrations are plain SQL files you commit to your repo:
-- migrations/0001_add_user_roles.sql
ALTER TABLE "users" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;
CREATE INDEX "users_role_idx" ON "users" ("role");
You can edit the SQL before running it, which is useful when you need to backfill data as part of a migration. Drizzle doesn’t regenerate the file unless you delete it.
Database and Runtime Support
Drizzle supports PostgreSQL, MySQL, and SQLite. The adapters cover most connection options:
| Adapter | Use Case |
|---|---|
drizzle-orm/node-postgres | Standard Postgres (pg library) |
drizzle-orm/neon-http | Neon serverless Postgres over HTTP |
drizzle-orm/postgres-js | postgres.js driver |
drizzle-orm/better-sqlite3 | Local SQLite |
drizzle-orm/libsql | Turso / LibSQL (SQLite at the edge) |
drizzle-orm/d1 | Cloudflare D1 |
drizzle-orm/planetscale-serverless | PlanetScale |
Setting up for Neon (common for serverless Postgres):
// src/db/index.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
Setting up for Cloudflare D1 (SQLite at the edge):
// In a Cloudflare Worker
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';
export default {
async fetch(request: Request, env: Env) {
const db = drizzle(env.DB, { schema });
const result = await db.select().from(schema.users).limit(10);
return Response.json(result);
},
};
This is where Drizzle has a concrete advantage over Prisma. The Prisma query engine doesn’t run on Cloudflare Workers or similar edge environments. Drizzle does. It’s pure TypeScript with no native binary.
Migrating From Prisma
If you have a Prisma schema, migration is mostly straightforward. The main translation:
// Prisma schema
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
Becomes:
// Drizzle schema
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
The query syntax changes are bigger. Prisma’s API:
// Prisma
const user = await prisma.user.findUnique({ where: { id: 1 } });
const users = await prisma.user.findMany({
where: { posts: { some: { published: true } } },
include: { posts: true },
});
Drizzle equivalents:
// Drizzle
const user = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, 1),
});
const usersWithPublishedPosts = await db.query.users.findMany({
with: {
posts: {
where: (posts, { eq }) => eq(posts.published, true),
},
},
});
Plan for a few hours per model in a complex schema. The relational query API covers most Prisma patterns.
Raw SQL When Needed
You can drop to raw SQL at any time without giving up type safety:
import { sql } from 'drizzle-orm';
const result = await db.execute(sql`
SELECT u.id, u.email, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
GROUP BY u.id, u.email
HAVING COUNT(p.id) > ${minPosts}
`);
The tagged template literal is SQL injection-safe. Values are parameterized, not interpolated.
When Not to Use Drizzle
Drizzle’s relational query API doesn’t yet match Prisma’s where nesting for complex nested filters. If your queries involve deep nested conditions across multiple relations, Prisma or raw SQL may be easier.
If your team is coming from a backend-agnostic ORM background (Sequelize, TypeORM) and needs familiarity, Prisma’s API is closer to those patterns.
For most projects starting fresh in TypeScript in 2026, Drizzle is worth the default choice: it’s lighter, more flexible, and the edge runtime support is a genuine differentiator as more infrastructure moves to serverless environments.
Sponsored
More from this category
More from Web Development
CSS Anchor Positioning: Tooltips and Popovers Without JavaScript
gRPC in 2026: When to Use It Instead of REST or GraphQL
k6 Load Testing: Performance Testing Your APIs Before Users Find the Problems
Sponsored
The dispatch
Working notes from
the studio.
A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored