drizzle-orm
A TypeScript ORM that stays close to SQL: you declare table schemas in TypeScript, then write queries that read like SQL but come back fully typed. It has zero runtime dependencies, is tree-shakeable, and works with every major Postgres, MySQL, and SQLite flavor including serverless ones (Neon, Turso, PlanetScale, Cloudflare D1) because it plugs into whatever driver you install. Migrations and schema diffing live in the separate drizzle-kit CLI, and Drizzle Studio adds a data browser. If you know SQL, you already know most of Drizzle.
The best TypeScript ORM for people who like SQL, and the right default for serverless and edge deployments. Go in with eyes open about the pre-1.0 churn and the open-issue backlog; if you want boring stability today, Prisma is the safer, heavier pick.
Use it if
- You know SQL and want types on top of it, not an abstraction that hides it: the query builder maps almost one-to-one onto SQL
- You deploy to edge or serverless runtimes (Cloudflare Workers, Vercel Edge, Deno, Bun) where Prisma-style engines and heavyweight clients hurt
- Bundle size matters: the core is about 8 KB gzipped with zero dependencies, and only the parts you use get bundled
- You want migrations generated by diffing your TypeScript schema (drizzle-kit generate) instead of writing SQL migration files by hand
- You want a stable 1.0: the latest stable is 0.45.2 (published March 2026) while v1.0 has sat in beta and rc for over a year, and pre-1.0 minor releases have shipped breaking changes before
- Your team does not know SQL: Drizzle deliberately does not hide joins, indexes, or dialect differences, so it is a worse fit than Prisma for SQL-averse teams
- You rely on issues getting answered: the repo has around 1,900 open issues and PRs against a small core team, so expect to debug edge cases yourself
- You need MongoDB or another non-SQL store: it is SQL-only (Postgres, MySQL, SQLite and their variants)
- You want one install: real usage means drizzle-orm plus drizzle-kit plus your driver, and the relational query API differs between the 0.x stable and the v1 rc, so tutorials frequently do not match your installed version
Setup reality
npm install drizzle-orm is never enough: you also install your database driver (pg, postgres, mysql2, better-sqlite3, or a serverless client; the package lists 25+ optional peer dependencies) and drizzle-kit as a dev dependency for migrations. Then you write a drizzle.config.ts pointing at your schema file and database URL. The generate/migrate/push workflow is good once learned, but docs and blog posts mix 0.x and v1-beta APIs (the relational query builder changed between them), and error messages from a wrong driver/dialect pairing can be cryptic. Type inference is excellent but heavy schemas can slow tsc noticeably.
Patterns
Define a Postgres table schemadefine-schema
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});Import column types from the dialect-specific module (pg-core, mysql-core, sqlite-core); they are not interchangeable.
Connect with the node-postgres driverconnect-database
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });The driver is a separate install (pg here); pass schema in the options or db.query.* relational queries will not exist.
Select rows with a filterselect-where
import { eq, and, gt } from 'drizzle-orm';
const admins = await db
.select()
.from(users)
.where(and(eq(users.role, 'admin'), gt(users.createdAt, since)))
.limit(20);Filters are functions (eq, and, gt) imported from drizzle-orm, not object literals like Prisma.
Insert a row and get it backinsert-returning
const [created] = await db
.insert(users)
.values({ name: 'Ada', email: 'ada@example.com' })
.returning();returning() works on Postgres and SQLite; MySQL does not support it, use $returningId() or a follow-up select there.
Update and delete with conditionsupdate-delete
await db
.update(users)
.set({ name: 'Ada Lovelace' })
.where(eq(users.id, 1));
await db.delete(users).where(eq(users.id, 1));Omitting where() updates or deletes every row in the table; Drizzle will not stop you.
Join two tablesjoin-tables
const rows = await db
.select({ user: users.name, post: posts.title })
.from(users)
.leftJoin(posts, eq(posts.authorId, users.id));Joins return flat row objects shaped by your select projection, not nested entities; use relational queries for nesting.
Fetch nested relationsrelational-query
// schema: define relations once
import { relations } from 'drizzle-orm';
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
// query: nested result, typed
const result = await db.query.users.findMany({
with: { posts: true },
limit: 10,
});This is the 0.45.x API; the v1 rc reworks the relational query builder, so check which version a tutorial targets.
Run queries in a transactiontransaction
await db.transaction(async (tx) => {
const [account] = await tx
.select()
.from(accounts)
.where(eq(accounts.id, from));
if (account.balance < amount) throw new Error('insufficient');
await tx.update(accounts).set({ balance: account.balance - amount }).where(eq(accounts.id, from));
});Throwing inside the callback rolls the transaction back; use tx, not db, for every query inside it.
Insert or update on conflictupsert
await db
.insert(users)
.values({ email: 'ada@example.com', name: 'Ada' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'Ada' },
});onConflictDoUpdate is Postgres/SQLite; MySQL uses onDuplicateKeyUpdate instead.
Drop to raw SQL safelyraw-sql
import { sql } from 'drizzle-orm';
const rows = await db.execute(
sql`select date_trunc('day', created_at) as day, count(*)::int as n
from users where created_at > ${since} group by 1 order by 1`
);Interpolations in the sql template are parameterized, not string-concatenated, so this stays injection-safe.
Generate and run migrations with drizzle-kitgenerate-migrations
# drizzle.config.ts points at schema + DATABASE_URL
npx drizzle-kit generate # diff schema -> SQL migration file
npx drizzle-kit migrate # apply pending migrations
npx drizzle-kit push # dev only: apply schema directly, no filespush is convenient in development but skips migration history; do not use it against production data.
Count rowscount-rows
import { count } from 'drizzle-orm';
const [{ value }] = await db
.select({ value: count() })
.from(users)
.where(eq(users.role, 'admin'));count() returns a number-typed column; older tutorials show sql`count(*)` casts that are no longer needed.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| prisma | npm | Your team prefers a schema DSL and generated client over SQL, and is not deploying to edge runtimes |
| kysely | npm | You want just a type-safe SQL query builder with no schema layer or migration opinions |
| typeorm | npm | A legacy codebase already uses it or you want decorator-based entities in a traditional Node app |
| knex | npm | Plain JavaScript projects that want a mature query builder and migrations without TypeScript inference |