drizzle-orm review
Our install of drizzle-orm 0.45.2 contained a large set of typed entry points for PostgreSQL, MySQL, SQLite, and hosted variants, yet no required runtime dependency. You declare tables in TypeScript, connect through a separately installed database driver, and compose SQL-shaped queries whose result types follow the selected columns and joins. Drizzle Kit handles schema diffs and migrations outside the runtime package. The current stable release is a security fix: 0.45.2 corrects escaping in sql.identifier() and sql.as() that could permit SQL injection. The 1.0 line is still on release-candidate tags, so examples written for its newer relational query API may not match the stable package.
Drizzle 0.45.2 is a good fit for TypeScript teams that want SQL to stay visible and need a driver-specific path into serverless databases. Install the current patch, budget for the separate driver and migration tool, and avoid 1.0 examples until you deliberately move to that release line.
We installed it
| Install | ✓ · 17.9s | 1 package on disk · 17 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 8.5 KB | gzipped (27.6 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does drizzle-orm install cleanly?
Yes. In a fresh container with an empty cache, npm install drizzle-orm finished in 18 seconds, leaving 1 package and 17 MB on disk. npm audit reported no known vulnerabilities.
How much does drizzle-orm add to a browser bundle?
8.5 KB gzipped (27.6 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does drizzle-orm work with both ESM and CommonJS?
Yes. Both import 'drizzle-orm' and require('drizzle-orm') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does drizzle-orm include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
drizzle-orm or prisma: which should you use?
prisma: Choose it when a schema DSL, generated client, nested relation API, and broader onboarding material matter more than a thin SQL-shaped layer. Drizzle 0.45.2 is a good fit for TypeScript teams that want SQL to stay visible and need a driver-specific path into serverless databases.
When should you not use drizzle-orm?
Your team wants an ORM to hide joins, indexes, dialect rules, and transaction details. Drizzle's query builder intentionally mirrors SQL concepts
Use it if
- Your TypeScript team already understands SQL and wants compile-time row shapes without an generated query client or resident engine
- The same schema layer must work with a small serverless driver such as Neon, libSQL, Cloudflare D1, PGlite, or PlanetScale
- You need control over selected columns, joins, conflict clauses, transactions, and raw SQL fragments while preserving parameter binding
- Schema-as-code and reviewable SQL migration files fit your deployment process better than a separate declarative schema language
- Your team wants an ORM to hide joins, indexes, dialect rules, and transaction details. Drizzle's query builder intentionally mirrors SQL concepts
- You need a settled 1.0 API today. npm still labels 0.45.2 as latest while a separate 1.0 release candidate changes parts of the relational query system
- MongoDB or another document store is the target. The supported model is SQL across PostgreSQL, MySQL, SQLite, and compatible services
- A small support queue is required for adoption. GitHub showed 1,981 open issues and pull requests, so uncommon driver and dialect failures can take time to triage
- You expect one dependency to provide the ORM, driver, migration CLI, and studio. drizzle-orm needs a chosen driver, and migration work normally adds drizzle-kit
Setup reality
We installed drizzle-orm 0.45.2 in a fresh Node 22 Bookworm container with no cache. npm completed in 17.9 seconds and left one package using 17 MB on disk. The published package is 16,984 KB unpacked, with zero direct dependencies and 28 peer dependencies. npm audit found zero known vulnerabilities. It declares ESM and an exports map, while both require() and ESM import worked. TypeScript declarations are bundled.
That single-package result is only the ORM layer. Pick one matching entry point and install its driver: drizzle-orm/node-postgres with pg, drizzle-orm/postgres-js with postgres, drizzle-orm/mysql2 with mysql2, or a service-specific adapter. Most of the 28 peers are optional choices rather than a list to install together. Passing your schema into drizzle() is required for db.query table access. Connection strings, TLS, pools, and serverless limits still belong to the selected driver.
Migration work adds drizzle-kit and drizzle.config.ts. The config points to schema files, selects the database dialect, and supplies connection credentials. generate writes SQL files from a schema diff; migrate applies recorded files; push changes the database directly and is a development convenience with a different review trail. Keep CI and local CLI versions aligned. Stable 0.45.x documentation and 1.0 release-candidate examples can disagree, particularly around relational queries, so check the installed version before copying code.
Our full-package browser build measured 27.6 KB minified and 8.5 KB gzipped. Tree shaking and a driver-specific import can make an application result different, but the disk install remains 17 MB because the package publishes many dialect and integration modules. There was no native build in the ORM itself. Runtime support depends on the driver: better-sqlite3 brings native binaries, pg expects TCP access, and edge adapters have their own transaction and connection constraints. Version 0.45.2 should be the floor because it fixes identifier escaping.
Patterns
Define a PostgreSQL table declare-postgres-table
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
export const accounts = pgTable("accounts", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
email: text().notNull().unique(),
createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(),
});Column builders are dialect-specific. Do not mix pg-core builders into a MySQL or SQLite schema.
Connect through node-postgres connect-postgres
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema.js";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle({ client: pool, schema });Install pg separately. Supplying schema enables the relational db.query API and does not replace pool sizing or TLS configuration.
Filter and limit a select select-filtered-rows
import { and, eq, gt } from "drizzle-orm";
const rows = await db
.select({ id: accounts.id, email: accounts.email })
.from(accounts)
.where(and(eq(accounts.active, true), gt(accounts.id, 100)))
.limit(25);The selected object controls the inferred result shape. Conditions use imported SQL operators rather than a filter object.
Insert and return a PostgreSQL row insert-returned-row
const [account] = await db
.insert(accounts)
.values({ email: "ada@example.com" })
.returning();PostgreSQL and SQLite support returning(). For MySQL, use the dialect's $returningId() behavior or issue a separate select.
Update rows behind a condition update-safely
import { eq } from "drizzle-orm";
await db
.update(accounts)
.set({ active: false })
.where(eq(accounts.id, accountId));The ORM permits an update without where(), which changes every row. Add an application guard when bulk updates are never valid.
Delete one row by primary key delete-safely
import { eq } from "drizzle-orm";
const removed = await db
.delete(accounts)
.where(eq(accounts.id, accountId))
.returning({ id: accounts.id });A missing where() deletes the full table. returning() is dialect-dependent and is unavailable on MySQL in this form.
Join with an explicit result shape join-projected-columns
const rows = await db
.select({
accountEmail: accounts.email,
invoiceTotal: invoices.totalCents,
})
.from(accounts)
.leftJoin(invoices, eq(invoices.accountId, accounts.id));A left join makes the joined fields nullable in the inferred type. The projection stays flat unless you deliberately nest its object keys.
Load a relation with the stable query API query-relations
const rows = await db.query.accounts.findMany({
with: { invoices: true },
where: (account, { eq }) => eq(account.active, true),
limit: 20,
});This shape targets stable 0.45.x and requires schema plus relation definitions in drizzle(). The 1.0 candidate has different relational-query material.
Commit several statements together run-transaction
await db.transaction(async (tx) => {
await tx.update(accounts)
.set({ balanceCents: sql`${accounts.balanceCents} - ${amount}` })
.where(eq(accounts.id, sourceId));
await tx.update(accounts)
.set({ balanceCents: sql`${accounts.balanceCents} + ${amount}` })
.where(eq(accounts.id, destinationId));
});Use tx for every statement inside the callback. Driver and platform capabilities determine isolation options and whether interactive transactions are supported.
Update on a PostgreSQL conflict upsert-postgres
await db.insert(accounts)
.values({ email, displayName })
.onConflictDoUpdate({
target: accounts.email,
set: { displayName },
});PostgreSQL and SQLite use onConflictDoUpdate. MySQL exposes onDuplicateKeyUpdate instead.
Parameterize a raw SQL expression compose-raw-sql
import { sql } from "drizzle-orm";
const result = await db.execute(sql`
select date_trunc('day', ${events.createdAt}) as day, count(*)::int as total
from ${events}
where ${events.createdAt} >= ${since}
group by 1
`);Values interpolated through the sql template become parameters. Use sql.identifier only for identifier data and stay on 0.45.2 or newer for its escaping fix.
Generate and apply migration files manage-migrations
# drizzle.config.ts identifies dialect, schema, output, and credentials
npx drizzle-kit generate
npx drizzle-kit migrategenerate creates reviewable SQL from schema changes; migrate applies recorded files. drizzle-kit push writes changes directly and is a different production-control choice.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| prisma | npm | Choose it when a schema DSL, generated client, nested relation API, and broader onboarding material matter more than a thin SQL-shaped layer |
| kysely | npm | Choose it when you want a typed query builder and prefer to keep schema declaration and migration policy elsewhere |
| sequelize | npm | Choose it for an established JavaScript application that relies on model instances, hooks, and its long-running plugin ecosystem |
| typeorm | npm | Choose it when decorator-based entities and Data Mapper or Active Record patterns already define the codebase |
More data guides
numpy · fsspec · pandas · pyarrow · sqlalchemy · s3fs · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

