cheat sheet
drizzle-orm
Package-level reference for Drizzle ORM — schema definitions, type-safe queries, relations, migrations with drizzle-kit, and deployment patterns.
drizzle-orm
What it is
drizzle-orm is a TypeScript SQL toolkit positioned between a query builder and an ORM. You define tables as TypeScript objects, and the library exposes a fluent query API that compiles to plain SQL — no hidden runtime, no abstract DBAL, no proxy magic. Schemas are the source of truth; types flow from the schema directly into queries, returning fully-typed rows.
Reach for drizzle-orm when you want type-safe SQL with control over the generated queries and lightweight deployment (it runs on Cloudflare Workers, Bun, Deno, Node, edge runtimes — anywhere your SQL driver runs). Reach for prisma if you want a heavier ORM with a generated client and richer migration UX; reach for kysely for an even more SQL-shaped query builder; reach for raw SQL when introspection isn't worth the abstraction.
Install
drizzle-orm plus a database driver plus drizzle-kit for migrations.
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg
Output: added drizzle-orm + pg driver and drizzle-kit + types as dev deps
pnpm add drizzle-orm postgres
pnpm add -D drizzle-kit
Output: added drizzle-orm with the postgres.js driver
bun add drizzle-orm @libsql/client
bun add -D drizzle-kit
Output: added drizzle-orm with the libSQL driver
yarn add drizzle-orm mysql2
yarn add -D drizzle-kit
Output: added drizzle-orm with the mysql2 driver
Versioning & Node support
Drizzle is approaching 1.0; current versions in the 0.4x line are production-grade and used widely.
- Node 18+ recommended; runs on Cloudflare Workers, Bun, Deno, and Vercel Edge.
- Dual ESM/CJS.
- TypeScript types bundled (the entire library is types-first).
- API has been mostly stable; specific dialect APIs (PostgreSQL vs MySQL vs SQLite) occasionally gain features per minor.
Package metadata
- Maintainer: Drizzle Team
- Project home: github.com/drizzle-team/drizzle-orm
- Docs: orm.drizzle.team
- npm: npmjs.com/package/drizzle-orm
- License: Apache 2.0
- First released: 2022
- Downloads: millions weekly and growing — the fastest-rising TypeScript ORM.
Peer dependencies & extras
drizzle-orm requires a driver of your choice as a peer:
- PostgreSQL:
pg,postgres(postgres.js),@neondatabase/serverless,@vercel/postgres,@cloudflare/workers-types+ Hyperdrive - MySQL:
mysql2,planetscale-database - SQLite:
better-sqlite3,@libsql/client,bun:sqlite,@cloudflare/workers-types+ D1 - Migrations:
drizzle-kit(dev dep)
Common extras:
drizzle-zod/drizzle-valibot/drizzle-typebox— derive schema validators from tablesdrizzle-seed— typed seeders
Alternatives
| Package | Trade-off |
|---|---|
prisma | Generated client, polished migration UX, heavier runtime. Edge support improving but historically Node-centric. |
kysely | Pure query builder, no migration tooling. Even thinner. |
typeorm | Mature decorators-based ORM. Older paradigm. |
mikro-orm | Unit-of-work ORM with identity maps. Heavier. |
objection | Active-record on Knex. Older. |
Raw pg / postgres | No abstraction. You write all the SQL and types. |
Real-world recipes
Schema definition
// db/schema.ts
import { pgTable, serial, text, integer, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: text("email").notNull().unique(),
name: text("name"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
authorId: integer("author_id").notNull().references(() => users.id),
title: text("title").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] }),
}));
Output: declarative table definitions; types like typeof users.$inferSelect give you fully-typed row shapes.
Connect and query
// db/client.ts
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 });
import { eq } from "drizzle-orm";
import { db } from "./client";
import { users } from "./schema";
const alice = await db.select().from(users).where(eq(users.email, "alice@example.com"));
console.log(alice[0]?.name);
Output: returns an array of fully-typed user rows.
Insert with returning
import { db } from "./client";
import { users } from "./schema";
const [newUser] = await db
.insert(users)
.values({ email: "alice@example.com", name: "Alice Dev" })
.returning();
console.log(newUser.id);
Output: inserts and returns the new row including the generated id.
Relational query (the query API)
import { db } from "./client";
const result = await db.query.users.findFirst({
where: (u, { eq }) => eq(u.email, "alice@example.com"),
with: { posts: true },
});
console.log(result?.posts.length);
Output: returns a user joined with their posts; the type is { ... posts: Post[] }.
Transaction
import { db } from "./client";
import { users, posts } from "./schema";
await db.transaction(async (tx) => {
const [u] = await tx.insert(users).values({ email: "alice@example.com" }).returning();
await tx.insert(posts).values({ authorId: u.id, title: "Hello" });
});
Output: both inserts commit atomically; any thrown error rolls back the transaction.
Migrations with drizzle-kit
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
schema: "./db/schema.ts",
out: "./drizzle/migrations",
dbCredentials: { url: process.env.DATABASE_URL! },
});
npx drizzle-kit generate # diffs schema vs migrations, writes a new SQL migration
npx drizzle-kit migrate # applies pending migrations
Output: generate writes a timestamped .sql file; migrate runs them against the connected database.
Cloudflare D1 + Workers
import { drizzle } from "drizzle-orm/d1";
import { eq } from "drizzle-orm";
import * as schema from "./schema";
type Bindings = { DB: D1Database };
export default {
async fetch(req: Request, env: Bindings) {
const db = drizzle(env.DB, { schema });
const users = await db.select().from(schema.users);
return Response.json(users);
},
};
Output: queries D1 from a Worker; the same Drizzle API as Postgres/MySQL/SQLite with the D1 adapter.
Pagination with limit/offset
import { db } from "./client";
import { posts } from "./schema";
import { desc } from "drizzle-orm";
const page = await db
.select()
.from(posts)
.orderBy(desc(posts.id))
.limit(20)
.offset(40);
Output: returns 20 posts ordered descending by id, starting from row 41.
Production deployment
Drizzle's deployment story is dictated by your driver and database, not by Drizzle itself.
- Postgres on Neon / Supabase / RDS. Use
pgorpostgres.jsdriver. Tune connection pool size to fit the platform's concurrent connection budget. - Postgres at the edge.
@neondatabase/serverless(HTTP-based) or@vercel/postgreswork in Cloudflare Workers and Vercel Edge — no TCP socket required. - Cloudflare D1. SQLite-compatible, scales automatically; deployed with
wrangler. - PlanetScale. Use the
planetscale-databasedriver. HTTP-based, edge-compatible. - Bun + Postgres. Bun's built-in postgres client pairs with Drizzle via
drizzle-orm/bun-postgres. - Migrations in CI/CD. Run
drizzle-kit migratein deploy hooks against the production DB. Wrap with locking / advisory locks if multiple deploys can race.
A safer pattern is to generate migrations locally, commit them to git, then apply at deploy time. Never auto-generate in production — the diff might be wrong if the live schema has drifted.
Performance tuning
- Prepared statements (
db.select().prepare()) for hot-path queries. Drizzle caches the SQL string and parameter shape. - Use
query.findMany/findFirstinstead of rawselectjoins for relational fetches — the query API is hand-optimised for the common case. - Avoid N+1. The relational query API issues a single SQL query (via
LEFT JOIN) or a small fixed number; manual code paths can blunder into N+1 trivially. - Connection pooling. Postgres
Poolwith a sensible max; for serverless, lean on a serverless-friendly driver (Neon HTTP, Hyperdrive) instead of a TCP pool. - Index design lives in SQL, not Drizzle. Add
index()directives in the table definition so they're tracked in migrations. selectonly the columns you need..select({ id: users.id, name: users.name }).from(users)skips fetching the rest.- Batch inserts with
.values([row1, row2, ...])— a single SQL statement is dramatically faster than per-row inserts.
Version migration guide
Drizzle pre-1.0 has had a steady stream of additive minor releases with occasional small breaks. Read the changelog for each bump.
| From | To | Key changes |
|---|---|---|
0.2x | 0.3x | Relational query API (db.query.users.findMany); migrations format updated. |
0.3x | 0.4x | Various dialect APIs tightened; transactions API stabilised; type inference improved. |
Before (older direct-select join):
const result = await db
.select({ user: users, post: posts })
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId));
After (relational query API):
const result = await db.query.users.findMany({ with: { posts: true } });
Output: equivalent data shape, much less boilerplate.
Migration checklist:
- Bump
drizzle-ormanddrizzle-kittogether. - Re-run
drizzle-kit generateto see if the new version emits cleaner migrations. - Audit any deprecation warnings.
- Re-run your test suite — type inference often surfaces real bugs after upgrades.
Security considerations
- Parameterised queries by default. Drizzle never concatenates user input into SQL. Stay on the typed query API — avoid
sql.rawwith user input. sql.rawis a SQL-injection vector when given untrusted input. Usesql(template tag) which parameterises automatically.- Migrations run with full privileges. Treat the migration user as a privileged credential — separate it from the application's runtime user where possible.
- Connection-string leakage. Never log
DATABASE_URL; redact in error reporting (Sentry beforeSend, etc.). - Mass assignment. Don't
.values(req.body)directly — pick the allowed fields. - Rate-limit destructive endpoints. Drizzle makes
DELETE FROM usersa one-liner; gate behind authorisation and rate limiting.
Testing & CI integration
Integration test against a real database
// db.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { users } from "./schema";
const pool = new Pool({ connectionString: process.env.TEST_DATABASE_URL });
const db = drizzle(pool);
beforeAll(async () => {
await db.insert(users).values({ email: "alice@example.com", name: "Alice" });
});
afterAll(async () => {
await db.delete(users);
await pool.end();
});
describe("users", () => {
it("finds Alice", async () => {
const rows = await db.select().from(users);
expect(rows.find((u) => u.email === "alice@example.com")).toBeDefined();
});
});
Output: integration test runs against a real test database; fixtures are cleaned up afterward.
Unit test with an in-memory SQLite
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
const sqlite = new Database(":memory:");
const db = drizzle(sqlite);
// run migrations programmatically against the in-memory DB
Output: fast unit tests with a real SQL engine; dialect differences mean this is not a perfect stand-in for Postgres.
CI pipeline
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: postgres }
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: "npm" }
- run: npm ci
- run: npx drizzle-kit migrate
env: { DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres }
- run: npm test -- --run
Ecosystem integrations
| Package | Role |
|---|---|
drizzle-zod / drizzle-valibot / drizzle-typebox | Schemas derived from tables |
drizzle-seed | Type-safe seeding |
@neondatabase/serverless | HTTP Postgres driver for edge |
@vercel/postgres | Vercel-managed Postgres |
@libsql/client | libSQL / Turso edge SQLite |
@cloudflare/workers-types | D1 + Hyperdrive types |
lucia-auth (and successors) | Sessions with Drizzle adapter |
next-auth | Auth.js Drizzle adapter |
Troubleshooting common errors
Cannot find module 'drizzle-kit' — install as a dev dep; only drizzle-orm is runtime.
relation "users" does not exist — migration wasn't applied. Run drizzle-kit migrate or drizzle-kit push against the target database.
Type ... not assignable to never when using select with non-existent columns — the column reference is wrong. Use the table object's accessors (users.email) not strings.
SyntaxError: Unexpected token from drizzle.config.ts — older Node without TS support; install tsx and run via npx tsx drizzle.config.ts.
Edge runtime error pg cannot be used in Workers — use @neondatabase/serverless or drizzle-orm/d1 instead.
generate produces a destructive migration unexpectedly — your local schema has drifted from the migrations folder. Inspect the diff, then either fix the schema or reset the migrations.
Relations not loading (undefined array) — schema not registered in drizzle(pool, { schema }) config. The relational query API requires the schema to be passed.
When NOT to use this
- You want a turn-key ORM with a generated client and richer migration UX. Prisma is more mature in that lane.
- Polyglot persistence (NoSQL + SQL). Drizzle is SQL-only.
- Pure SQL workflow with no ORM benefits. A bare driver (
pg,postgres) is leaner. - Heavy stored-procedure-driven application. ORMs in general add little value when the database does the work.
See also
- JavaScript: prisma — comparator ORM
- Concept: json — typing data at boundaries