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.

bash
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

bash
pnpm add drizzle-orm postgres
pnpm add -D drizzle-kit

Output: added drizzle-orm with the postgres.js driver

bash
bun add drizzle-orm @libsql/client
bun add -D drizzle-kit

Output: added drizzle-orm with the libSQL driver

bash
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 tables
  • drizzle-seed — typed seeders

Alternatives

PackageTrade-off
prismaGenerated client, polished migration UX, heavier runtime. Edge support improving but historically Node-centric.
kyselyPure query builder, no migration tooling. Even thinner.
typeormMature decorators-based ORM. Older paradigm.
mikro-ormUnit-of-work ORM with identity maps. Heavier.
objectionActive-record on Knex. Older.
Raw pg / postgresNo abstraction. You write all the SQL and types.

Real-world recipes

Schema definition

typescript
// 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

typescript
// 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 });
typescript
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

typescript
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)

typescript
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

typescript
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

typescript
// 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! },
});
bash
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

typescript
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

typescript
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 pg or postgres.js driver. Tune connection pool size to fit the platform's concurrent connection budget.
  • Postgres at the edge. @neondatabase/serverless (HTTP-based) or @vercel/postgres work in Cloudflare Workers and Vercel Edge — no TCP socket required.
  • Cloudflare D1. SQLite-compatible, scales automatically; deployed with wrangler.
  • PlanetScale. Use the planetscale-database driver. 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 migrate in 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 / findFirst instead of raw select joins 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 Pool with 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.
  • select only 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.

FromToKey changes
0.2x0.3xRelational query API (db.query.users.findMany); migrations format updated.
0.3x0.4xVarious dialect APIs tightened; transactions API stabilised; type inference improved.

Before (older direct-select join):

typescript
const result = await db
  .select({ user: users, post: posts })
  .from(users)
  .leftJoin(posts, eq(users.id, posts.authorId));

After (relational query API):

typescript
const result = await db.query.users.findMany({ with: { posts: true } });

Output: equivalent data shape, much less boilerplate.

Migration checklist:

  1. Bump drizzle-orm and drizzle-kit together.
  2. Re-run drizzle-kit generate to see if the new version emits cleaner migrations.
  3. Audit any deprecation warnings.
  4. 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.raw with user input.
  • sql.raw is a SQL-injection vector when given untrusted input. Use sql (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 users a one-liner; gate behind authorisation and rate limiting.

Testing & CI integration

Integration test against a real database

typescript
// 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

typescript
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

yaml
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

PackageRole
drizzle-zod / drizzle-valibot / drizzle-typeboxSchemas derived from tables
drizzle-seedType-safe seeding
@neondatabase/serverlessHTTP Postgres driver for edge
@vercel/postgresVercel-managed Postgres
@libsql/clientlibSQL / Turso edge SQLite
@cloudflare/workers-typesD1 + Hyperdrive types
lucia-auth (and successors)Sessions with Drizzle adapter
next-authAuth.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