Drizzle ORM: A Modern, Type‑Safe ORM for JavaScript/TypeScript
Drizzle ORM has grown rapidly within the JavaScript and TypeScript ecosystem. Its design prioritizes type‑safety, simplicity, and performance—offering a powerful alternative to heavy, dependency‑driven ORMs. Drizzle is compact, predictable, and integrates naturally into modern backend environments like Node.js, serverless platforms, and edge runtimes.
This guide provides a practical, updated overview of Drizzle ORM: what it does well, how to install it, how to define schemas, and how to write clean, type‑safe queries.
What Is Drizzle ORM?
Drizzle is a lightweight relational ORM built for JavaScript and TypeScript. Supporting PostgreSQL, MySQL, and SQLite, it emphasizes stability, small bundle size, and strong typing. Its API resembles SQL more than traditional Active‑Record patterns, making queries explicit and predictable.
Key Features
- Compact & dependency‑free — around 7.4 KB.
- Fully typed — schemas, queries, and relations all use TypeScript inference.
- Environment‑ready — works in Node.js, serverless, edge runtimes, and browsers.
- Migrations — Drizzle Kit provides easy schema migrations and introspection.
- Graphical Studio — a visual database manager suitable for PostgreSQL, Neon, PlanetScale, and Turso.
Installing Drizzle ORM
Use your preferred package manager:
npm install drizzle-ormInstall the appropriate database driver:
npm install pg # PostgreSQL
npm install mysql2 # MySQL
npm install better-sqlite3 # SQLiteIf using Drizzle Kit for migrations:
npm install -D drizzle-kitDefining a Schema (PostgreSQL Example)
Schemas in Drizzle are declared via functions that closely mimic SQL structure.
import { pgTable, serial, varchar } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
firstName: varchar("first_name", { length: 256 }),
});Schemas are fully typed—queries against users.firstName will autocomplete and validate types.
Database Initialization
Establishing a connection is straightforward:
import { drizzle } from "drizzle-orm/node-postgres";
import { Client } from "pg";
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
export const db = drizzle(client);You can optionally set casing conventions:
const db = drizzle(client, { casing: "snake_case" });Simple Select Query
const rows = await db.select().from(users);Produces SQL similar to:
SELECT "id", "first_name" FROM "users";Shared Structures Between Tables
Use helpers to reduce duplication:
// columns.helpers.ts
import { timestamp } from "drizzle-orm/pg-core";
export const timestamps = {
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at"),
deletedAt: timestamp("deleted_at"),
};// users.sql.ts
import { pgTable, integer } from "drizzle-orm/pg-core";
import { timestamps } from "./columns.helpers";
export const users = pgTable("users", {
id: integer("id").primaryKey(),
...timestamps,
});Full Schema with Relations, Enums & Indexes
import * as t from "drizzle-orm/pg-core";
import { pgEnum, pgTable as table } from "drizzle-orm/pg-core";
export const roles = pgEnum("roles", ["guest", "user", "admin"]);
export const users = table(
"users",
{
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
firstName: t.varchar("first_name", { length: 256 }),
lastName: t.varchar("last_name", { length: 256 }),
email: t.varchar().notNull(),
role: roles().default("guest"),
invitee: t.integer().references(() => users.id),
},
table => [t.uniqueIndex("email_idx").on(table.email)]
);
export const posts = table(
"posts",
{
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
slug: t.varchar("slug").$default(() => crypto.randomUUID()),
title: t.varchar({ length: 256 }),
ownerId: t.integer("owner_id").references(() => users.id),
},
table => [
t.uniqueIndex("slug_idx").on(table.slug),
t.index("title_idx").on(table.title),
]
);
export const comments = table("comments", {
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
text: t.varchar({ length: 256 }),
postId: t.integer("post_id").references(() => posts.id),
ownerId: t.integer("owner_id").references(() => users.id),
});Select Queries & Joins
Basic Left Join
const result = await db
.select()
.from(posts)
.leftJoin(comments, (c, p) => c.postId.eq(p.id))
.where(posts.id.eq(10));Dynamic Filters
import { and, ilike, eq, lte } from "drizzle-orm";
async function getProducts({ name, category, maxPrice }) {
const filters = [];
if (name) filters.push(ilike(products.name, `%${name}%`));
if (category) filters.push(eq(products.category, category));
if (maxPrice) filters.push(lte(products.price, maxPrice));
return db.select().from(products).where(and(...filters));
}Subqueries
const staffView = db
.select()
.from(internalStaff)
.leftJoin(customUser, (s, u) => s.userId.eq(u.id))
.as("staff_view");
const tickets = await db
.select()
.from(ticket)
.leftJoin(staffView, (v, t) => v.internal_staff.userId.eq(t.staffId));CRUD Operations
Insert
await db.insert(users).values({ firstName: "Masha" });Select
const allUsers = await db.select().from(users);Update
await db.update(users).set({ firstName: "Vasya" }).where(eq(users.firstName, "Masha"));Delete
await db.delete(users).where(eq(users.firstName, "Vasya"));Conclusion
Drizzle ORM succeeds by being small, predictable, and type‑accurate. Unlike traditional ORMs, it avoids magic abstractions and instead exposes SQL-like primitives with strong TypeScript inference. Whether you are building an API server, a serverless function, or an edge application, Drizzle offers the performance and clarity needed to work efficiently with relational data.
Its combination of type‑safe schemas, clear migration tools, and SQL‑first query builder makes it one of the most practical ORMs available for modern JavaScript and TypeScript development.