Drizzle ORM
Drizzle is a SQL-first ORM for TypeScript: table definitions live in code, queries feel like SQL, and the runtime stays thin over node-postgres.
Recipe
Quick-reference recipe card - copy-paste ready.
// src/db/schema.ts
import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const orders = pgTable("orders", {
id: text("id").primaryKey(),
userId: text("user_id").notNull().references(() => users.id),
totalCents: integer("total_cents").notNull(),
});import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";
import { eq } from "drizzle-orm";
import { users } from "./schema";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool);
export async function findUserByEmail(email: string) {
const rows = await db.select().from(users).where(eq(users.email, email)).limit(1);
return rows[0] ?? null;
}When to reach for this:
- Developers want SQL visibility with TypeScript inference
- Bundle size and runtime overhead matter (edge workers, small services)
- Team comfortable reviewing generated migration SQL
Working Example
// src/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";
import * as schema from "./schema";
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
});
export const db = drizzle(pool, { schema });
// src/repositories/order-repo.ts
import { db } from "../db";
import { orders, orderItems } from "../db/schema";
import { eq } from "drizzle-orm";
export async function createOrder(
userId: string,
items: { sku: string; qty: number; priceCents: number }[]
) {
const totalCents = items.reduce((s, i) => s + i.qty * i.priceCents, 0);
return db.transaction(async (tx) => {
const [order] = await tx
.insert(orders)
.values({ id: crypto.randomUUID(), userId, totalCents })
.returning();
await tx.insert(orderItems).values(
items.map((i) => ({
id: crypto.randomUUID(),
orderId: order.id,
sku: i.sku,
qty: i.qty,
priceCents: i.priceCents,
}))
);
return order;
});
}What this demonstrates:
- Schema as TypeScript tables
db.transactionwraps Postgres transactionreturning()for INSERT results without second query- Same
pgpool tuning applies underneath
Deep Dive
Migrations with drizzle-kit
npx drizzle-kit generate
npx drizzle-kit migrate- Generated SQL lives in
drizzle/folder for PR review - Align CI to run migrate before app deploy, same as Prisma policy
Joins and Relations
import { db } from "./db";
import { users, orders } from "./schema";
import { eq } from "drizzle-orm";
const rows = await db
.select({
userId: users.id,
email: users.email,
orderId: orders.id,
totalCents: orders.totalCents,
})
.from(users)
.innerJoin(orders, eq(orders.userId, users.id))
.where(eq(users.id, userId));- Explicit joins make query plans reviewable in logs
- Relational query API (
db.query.users.findMany({ with: { orders: true } })) available when preferred
SQL Escape Hatch
import { sql } from "drizzle-orm";
await db.execute(sql`
SELECT date_trunc('day', created_at) AS day, COUNT(*)::int AS cnt
FROM orders GROUP BY 1
`);- Tagged
sqltemplate keeps parameterization for dynamic fragments
Fastify 5 Plugin Sketch
import fp from "fastify-plugin";
import { db } from "./db";
export const dbPlugin = fp(async (fastify) => {
fastify.decorate("db", db);
});- Decorate once; routes use
fastify.dbfor queries - Close pool in
onClosehook
Gotchas
- Forgetting
.limit(1)on unique lookups - returns array always. Fix:limit(1)and take[0]. - Schema drift without migrate in CI - runtime errors on missing columns. Fix: migrate deploy gate.
- Mixing Drizzle and Prisma in one service - two migration systems. Fix: pick one ORM per deployable.
- Large
inArrayfilters - huge SQL packets. Fix: batch IDs or temp table pattern. - No default UUID in DB - app must generate IDs consistently.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Prisma | Studio, relation ergonomics, larger ecosystem | Want SQL-first minimal runtime |
| Kysely | Query builder only, no schema DSL | Want integrated schema + migrate kit |
pg + SQL files | DBAs own all SQL | Need TS inference on columns |
| TypeORM | Legacy Nest projects | Greenfield without migration cost |
FAQs
Drizzle vs Prisma for new Node APIs?
Drizzle if SQL transparency and runtime weight matter. Prisma if schema DSL and tooling speed win.
Does Drizzle support NestJS 11?
Yes via custom provider wrapping db instance or community Drizzle modules.
How do I log SQL?
Enable logger in drizzle(pool, { logger: true }) in development only.
Serverless Postgres?
Small max on pool or serverless driver; see Connection Pool Tuning.
Can I use Drizzle with SQLite for tests?
Yes - swap drizzle-orm/better-sqlite3 driver in test bootstrap.
Composite primary keys?
Supported in table definition with tuple primaryKey config.
Enums?
pgEnum in schema maps to Postgres ENUM type - migrate carefully in prod.
How do I avoid N+1?
Use joins or with relational queries in one round trip - same discipline as Prisma include.
ESM on Node 24?
Drizzle is ESM-friendly. Use "type": "module" and import syntax throughout.
Raw performance vs Prisma?
Drizzle is typically lighter; benchmark your endpoints, not hello-world inserts.
Related
- Prisma - schema-first ORM
- node-postgres (
pg) - underlying driver - Databases Basics - when SQL vs document
- Connection Pool Tuning - pool sizing
- Databases Best Practices - migration policy
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.