Skip to content

Web Development · Backend

Drizzle ORM: TypeScript-First Database Access That Gets Out of Your Way

Drizzle ORM has become the go-to choice for TypeScript projects that want type safety without the overhead of a heavy ORM. Here's how it works, how to migrate from Prisma, and when to use it.

Anurag Verma

Anurag Verma

6 min read

Drizzle ORM: TypeScript-First Database Access That Gets Out of Your Way

Sponsored

Share

Prisma made type-safe database access mainstream in the Node.js ecosystem. But it came with trade-offs: a non-standard schema format, a heavy query engine binary, and limited flexibility when you needed to write SQL that its abstractions didn’t cover.

Drizzle ORM takes a different approach. Your schema is TypeScript. Your migrations are SQL. Your queries look close to SQL. The library stays thin: no separate binary, no generated files to check into git, no runtime code generation. You write TypeScript and get fully typed database calls.

It’s gained enough traction to be worth understanding, especially for projects where you want to run on edge runtimes like Cloudflare Workers or Vercel Edge, where Prisma’s query engine has historically caused problems.

How Drizzle Thinks About Schema

In Prisma, you define your schema in .prisma files, a custom DSL. Drizzle defines schema in TypeScript:

// src/db/schema.ts
import { pgTable, serial, text, integer, timestamp, boolean } from 'drizzle-orm/pg-core';

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

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  body: text('body'),
  authorId: integer('author_id').references(() => users.id).notNull(),
  publishedAt: timestamp('published_at'),
});

This file is just TypeScript. Your editor understands it. You can import it, write utilities around it, and use it in tests without any special tooling. The schema types flow through into your queries automatically.

Querying

Drizzle provides two query APIs: the SQL-like query builder and a relational query API for joins.

Query builder:

import { db } from './db';
import { users, posts } from './schema';
import { eq, and, gte, desc } from 'drizzle-orm';

// Select with filter
const activeUsers = await db
  .select()
  .from(users)
  .where(eq(users.isActive, true));

// The return type is inferred: { id: number; email: string; name: string | null; ... }[]

// Insert
const [newUser] = await db
  .insert(users)
  .values({ email: 'dev@example.com', name: 'Alice' })
  .returning();

// Update
await db
  .update(users)
  .set({ name: 'Alice Smith' })
  .where(eq(users.id, newUser.id));

// Delete
await db
  .delete(users)
  .where(and(
    eq(users.isActive, false),
    gte(users.createdAt, cutoffDate),
  ));

Relational queries (joins without writing JOIN):

For this to work, you define relations in the schema:

import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

Then query with nested includes:

const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: {
      where: (posts, { isNull }) => isNull(posts.publishedAt),
      columns: {
        id: true,
        title: true,
      },
    },
  },
  where: (users, { eq }) => eq(users.isActive, true),
});

// Type: { id: number; email: string; posts: { id: number; title: string }[] }[]

The return type is precisely typed based on what you included. If you only select id and title, you won’t get body in the result type.

Migrations With Drizzle Kit

Schema changes generate SQL migrations rather than running them automatically. This is intentional: you can review the migration before running it.

# Install Drizzle Kit (dev dependency)
npm install -D drizzle-kit

# Generate migration from schema changes
npx drizzle-kit generate

# Apply migrations
npx drizzle-kit migrate

The generated migrations are plain SQL files you commit to your repo:

-- migrations/0001_add_user_roles.sql
ALTER TABLE "users" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;
CREATE INDEX "users_role_idx" ON "users" ("role");

You can edit the SQL before running it, which is useful when you need to backfill data as part of a migration. Drizzle doesn’t regenerate the file unless you delete it.

Database and Runtime Support

Drizzle supports PostgreSQL, MySQL, and SQLite. The adapters cover most connection options:

AdapterUse Case
drizzle-orm/node-postgresStandard Postgres (pg library)
drizzle-orm/neon-httpNeon serverless Postgres over HTTP
drizzle-orm/postgres-jspostgres.js driver
drizzle-orm/better-sqlite3Local SQLite
drizzle-orm/libsqlTurso / LibSQL (SQLite at the edge)
drizzle-orm/d1Cloudflare D1
drizzle-orm/planetscale-serverlessPlanetScale

Setting up for Neon (common for serverless Postgres):

// src/db/index.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

Setting up for Cloudflare D1 (SQLite at the edge):

// In a Cloudflare Worker
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';

export default {
  async fetch(request: Request, env: Env) {
    const db = drizzle(env.DB, { schema });
    const result = await db.select().from(schema.users).limit(10);
    return Response.json(result);
  },
};

This is where Drizzle has a concrete advantage over Prisma. The Prisma query engine doesn’t run on Cloudflare Workers or similar edge environments. Drizzle does. It’s pure TypeScript with no native binary.

Migrating From Prisma

If you have a Prisma schema, migration is mostly straightforward. The main translation:

// Prisma schema
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

Becomes:

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

The query syntax changes are bigger. Prisma’s API:

// Prisma
const user = await prisma.user.findUnique({ where: { id: 1 } });
const users = await prisma.user.findMany({
  where: { posts: { some: { published: true } } },
  include: { posts: true },
});

Drizzle equivalents:

// Drizzle
const user = await db.query.users.findFirst({
  where: (users, { eq }) => eq(users.id, 1),
});

const usersWithPublishedPosts = await db.query.users.findMany({
  with: {
    posts: {
      where: (posts, { eq }) => eq(posts.published, true),
    },
  },
});

Plan for a few hours per model in a complex schema. The relational query API covers most Prisma patterns.

Raw SQL When Needed

You can drop to raw SQL at any time without giving up type safety:

import { sql } from 'drizzle-orm';

const result = await db.execute(sql`
  SELECT u.id, u.email, COUNT(p.id) AS post_count
  FROM users u
  LEFT JOIN posts p ON p.author_id = u.id
  GROUP BY u.id, u.email
  HAVING COUNT(p.id) > ${minPosts}
`);

The tagged template literal is SQL injection-safe. Values are parameterized, not interpolated.

When Not to Use Drizzle

Drizzle’s relational query API doesn’t yet match Prisma’s where nesting for complex nested filters. If your queries involve deep nested conditions across multiple relations, Prisma or raw SQL may be easier.

If your team is coming from a backend-agnostic ORM background (Sequelize, TypeORM) and needs familiarity, Prisma’s API is closer to those patterns.

For most projects starting fresh in TypeScript in 2026, Drizzle is worth the default choice: it’s lighter, more flexible, and the edge runtime support is a genuine differentiator as more infrastructure moves to serverless environments.

Sponsored

Enjoyed it? Pass it on.

Share this article.

Sponsored

The dispatch

Working notes from
the studio.

A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored