mrkeyoor.com_
Wed 05 Aug 05:03 UTC
npmDataupdated 05 Aug 2026

drizzle-orm

A TypeScript ORM that stays close to SQL: you declare table schemas in TypeScript, then write queries that read like SQL but come back fully typed. It has zero runtime dependencies, is tree-shakeable, and works with every major Postgres, MySQL, and SQLite flavor including serverless ones (Neon, Turso, PlanetScale, Cloudflare D1) because it plugs into whatever driver you install. Migrations and schema diffing live in the separate drizzle-kit CLI, and Drizzle Studio adds a data browser. If you know SQL, you already know most of Drizzle.

Verdict

The best TypeScript ORM for people who like SQL, and the right default for serverless and edge deployments. Go in with eyes open about the pre-1.0 churn and the open-issue backlog; if you want boring stability today, Prisma is the safer, heavier pick.

API stability3/5Still 0.x after years, with breaking changes in past minor releases; v1.0 has been in beta/rc since 2025 and its relational query API differs from 0.x, so code and tutorials split across two shapes
Docs4/5orm.drizzle.team has thorough per-dialect and per-provider guides plus benchmarks; the pain is version skew between 0.x docs, v1 beta docs, and third-party tutorials
Maintenance4/5Very active: pushed the day of this review with steady rc releases toward v1.0; but roughly 1,900 open issues and PRs against a small team means slow triage
Ecosystem4/518M weekly downloads, 35k stars, first-class adapters for every major serverless database, drizzle-kit and Studio tooling; younger and thinner plugin scene than Prisma or TypeORM

Use it if

  • You know SQL and want types on top of it, not an abstraction that hides it: the query builder maps almost one-to-one onto SQL
  • You deploy to edge or serverless runtimes (Cloudflare Workers, Vercel Edge, Deno, Bun) where Prisma-style engines and heavyweight clients hurt
  • Bundle size matters: the core is about 8 KB gzipped with zero dependencies, and only the parts you use get bundled
  • You want migrations generated by diffing your TypeScript schema (drizzle-kit generate) instead of writing SQL migration files by hand
Skip it if

Setup reality

npm install drizzle-orm is never enough: you also install your database driver (pg, postgres, mysql2, better-sqlite3, or a serverless client; the package lists 25+ optional peer dependencies) and drizzle-kit as a dev dependency for migrations. Then you write a drizzle.config.ts pointing at your schema file and database URL. The generate/migrate/push workflow is good once learned, but docs and blog posts mix 0.x and v1-beta APIs (the relational query builder changed between them), and error messages from a wrong driver/dialect pairing can be cryptic. Type inference is excellent but heavy schemas can slow tsc noticeably.

Patterns

Define a Postgres table schemadefine-schema

import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
});

Import column types from the dialect-specific module (pg-core, mysql-core, sqlite-core); they are not interchangeable.

Connect with the node-postgres driverconnect-database

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

The driver is a separate install (pg here); pass schema in the options or db.query.* relational queries will not exist.

Select rows with a filterselect-where

import { eq, and, gt } from 'drizzle-orm';

const admins = await db
  .select()
  .from(users)
  .where(and(eq(users.role, 'admin'), gt(users.createdAt, since)))
  .limit(20);

Filters are functions (eq, and, gt) imported from drizzle-orm, not object literals like Prisma.

Insert a row and get it backinsert-returning

const [created] = await db
  .insert(users)
  .values({ name: 'Ada', email: 'ada@example.com' })
  .returning();

returning() works on Postgres and SQLite; MySQL does not support it, use $returningId() or a follow-up select there.

Update and delete with conditionsupdate-delete

await db
  .update(users)
  .set({ name: 'Ada Lovelace' })
  .where(eq(users.id, 1));

await db.delete(users).where(eq(users.id, 1));

Omitting where() updates or deletes every row in the table; Drizzle will not stop you.

Join two tablesjoin-tables

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

Joins return flat row objects shaped by your select projection, not nested entities; use relational queries for nesting.

Fetch nested relationsrelational-query

// schema: define relations once
import { relations } from 'drizzle-orm';
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

// query: nested result, typed
const result = await db.query.users.findMany({
  with: { posts: true },
  limit: 10,
});

This is the 0.45.x API; the v1 rc reworks the relational query builder, so check which version a tutorial targets.

Run queries in a transactiontransaction

await db.transaction(async (tx) => {
  const [account] = await tx
    .select()
    .from(accounts)
    .where(eq(accounts.id, from));
  if (account.balance < amount) throw new Error('insufficient');
  await tx.update(accounts).set({ balance: account.balance - amount }).where(eq(accounts.id, from));
});

Throwing inside the callback rolls the transaction back; use tx, not db, for every query inside it.

Insert or update on conflictupsert

await db
  .insert(users)
  .values({ email: 'ada@example.com', name: 'Ada' })
  .onConflictDoUpdate({
    target: users.email,
    set: { name: 'Ada' },
  });

onConflictDoUpdate is Postgres/SQLite; MySQL uses onDuplicateKeyUpdate instead.

Drop to raw SQL safelyraw-sql

import { sql } from 'drizzle-orm';

const rows = await db.execute(
  sql`select date_trunc('day', created_at) as day, count(*)::int as n
      from users where created_at > ${since} group by 1 order by 1`
);

Interpolations in the sql template are parameterized, not string-concatenated, so this stays injection-safe.

Generate and run migrations with drizzle-kitgenerate-migrations

# drizzle.config.ts points at schema + DATABASE_URL
npx drizzle-kit generate   # diff schema -> SQL migration file
npx drizzle-kit migrate    # apply pending migrations
npx drizzle-kit push       # dev only: apply schema directly, no files

push is convenient in development but skips migration history; do not use it against production data.

Count rowscount-rows

import { count } from 'drizzle-orm';

const [{ value }] = await db
  .select({ value: count() })
  .from(users)
  .where(eq(users.role, 'admin'));

count() returns a number-typed column; older tutorials show sql`count(*)` casts that are no longer needed.

Alternatives

PackageRegistryPick it when
prismanpmYour team prefers a schema DSL and generated client over SQL, and is not deploying to edge runtimes
kyselynpmYou want just a type-safe SQL query builder with no schema layer or migration opinions
typeormnpmA legacy codebase already uses it or you want decorator-based entities in a traditional Node app
knexnpmPlain JavaScript projects that want a mature query builder and migrations without TypeScript inference