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.
Search across all documentation pages
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.
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:
// 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:
db.transaction wraps Postgres transactionreturning() for INSERT results without second querypg pool tuning applies underneathnpx drizzle-kit generate
npx drizzle-kit migratedrizzle/ folder for PR reviewimport { 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));db.query.users.findMany({ with: { orders: true } })) available when preferredimport { 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
`);sql template keeps parameterization for dynamic fragmentsimport fp from "fastify-plugin";
import { db } from "./db";
export const dbPlugin = fp(async (fastify) => {
fastify.decorate("db", db);
});fastify.db for queriesonClose hook.limit(1) on unique lookups - returns array always. Fix: limit(1) and take [0].inArray filters - huge SQL packets. Fix: batch IDs or temp table pattern.| 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 |
Drizzle if SQL transparency and runtime weight matter. Prisma if schema DSL and tooling speed win.
Yes via custom provider wrapping db instance or community Drizzle modules.
Enable logger in drizzle(pool, { logger: true }) in development only.
Small max on pool or serverless driver; see Connection Pool Tuning.
Yes - swap drizzle-orm/better-sqlite3 driver in test bootstrap.
Supported in table definition with tuple primaryKey config.
pgEnum in schema maps to Postgres ENUM type - migrate carefully in prod.
Use joins or with relational queries in one round trip - same discipline as Prisma include.
Drizzle is ESM-friendly. Use "type": "module" and import syntax throughout.
Drizzle is typically lighter; benchmark your endpoints, not hello-world inserts.
pg) - underlying driverStack 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.
Reviewed by Chris St. John·Last updated Jul 16, 2026