Web Development · Backend
Drizzle ORM: TypeScript-First Database Access
Drizzle is the TypeScript-first alternative to Prisma: schema in TS, migrations in SQL, no query engine binary. How it works, and when to pick 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.
Frequently asked questions
- Why does Drizzle work on Cloudflare Workers when Prisma doesn't?
- Because it is pure TypeScript with no native binary. Prisma's query engine is a compiled Rust binary that the Node process shells out to, and edge runtimes do not let you run one. Drizzle compiles queries in JavaScript and hands SQL to a driver, so anything with a driver that speaks HTTP or the platform's own binding works, which is what makes D1, Neon over HTTP, and Turso viable targets.
- Is generating migrations instead of applying them a downside?
- It is the point. drizzle-kit generate writes a plain SQL file you read before anything touches the database, and because it will not regenerate that file unless you delete it, you can edit it. That matters for the case every team eventually hits: a schema change that needs a data backfill in the same migration. An ORM that applies changes automatically gives you nowhere to put that.
- How long does migrating from Prisma take?
- Plan a few hours per model on a complex schema. The schema definitions translate almost mechanically. The queries take the time, because Prisma's findMany with nested where and include maps onto Drizzle's relational API differently enough that you rewrite rather than rename. The relational API covers most Prisma patterns; the ones it does not are usually simpler as raw SQL anyway.
- Do I lose type safety when I write raw SQL?
- No, and you do not lose injection safety either. The sql tagged template parameterizes every interpolated value rather than concatenating it into the string, so a user-supplied value cannot alter the query structure. You can annotate the expected result type, which means the escape hatch behaves like the rest of the library instead of dropping you back to untyped rows.
- When should I stay on Prisma?
- When your queries involve deeply nested conditions across several relations, because Drizzle's relational query API does not match Prisma's where nesting there yet. Also when the team's mental model comes from Sequelize or TypeORM, since Prisma's API is closer to those patterns and familiarity is a real cost. For a fresh TypeScript project in 2026, particularly one that might deploy to the edge, Drizzle is the better default.
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored