Skip to main content
← All Articles Β· Β· 13 min read

Drizzle ORM Complete Guide: TypeScript Database Toolkit 2026

Master Drizzle ORM 2026: schema definition, migrations, relations, RLS support, and Drizzle Studio. See how it compares to Prisma for TypeScript projects.

drizzle-ormtypescriptdatabaseormpostgresqlprismasql

Drizzle ORM has become the go-to TypeScript database toolkit for developers who want type safety without sacrificing SQL control. In 2026, it’s the default choice for new Cloudflare Workers, Neon, and PlanetScale projects β€” and for good reason. This guide covers everything you need to go from zero to production.


Why Drizzle?

Before Drizzle, TypeScript developers faced a hard choice: use Prisma for type safety and pay the performance/bundle size tax, or drop down to raw SQL and lose type inference. Drizzle solves this by making your schema the source of truth and generating types at compile time, not runtime.

Key design principles:

  • SQL-first: Drizzle’s query API mirrors SQL closely β€” if you know SQL, Drizzle feels natural
  • Zero runtime overhead: No reflection, no proxy objects, just compiled TypeScript
  • Single source of truth: Your schema file defines both the database structure and TypeScript types
  • Serverless-ready: Works with Cloudflare D1, Turso, Neon, PlanetScale out of the box

Installation and Setup

npm install drizzle-orm
npm install -D drizzle-kit

# For PostgreSQL
npm install postgres

# For MySQL
npm install mysql2

# For SQLite (or Turso/Cloudflare D1)
npm install better-sqlite3

Connecting to PostgreSQL

// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

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

For Neon serverless:

import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);

Schema Definition

Drizzle schemas are plain TypeScript β€” no decorators, no magic. You define tables using column builders:

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

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: text('name').notNull(),
  role: text('role', { enum: ['admin', 'user', 'moderator'] }).default('user'),
  metadata: jsonb('metadata').$type<{ preferences: Record<string, string> }>(),
  emailVerified: boolean('email_verified').default(false),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: varchar('title', { length: 500 }).notNull(),
  slug: varchar('slug', { length: 500 }).notNull().unique(),
  content: text('content'),
  published: boolean('published').default(false),
  authorId: uuid('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  publishedAt: timestamp('published_at'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

// Drizzle infers types automatically
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;

MySQL Schema

import { mysqlTable, int, varchar, boolean, datetime } from 'drizzle-orm/mysql-core';

export const products = mysqlTable('products', {
  id: int('id').primaryKey().autoincrement(),
  name: varchar('name', { length: 255 }).notNull(),
  price: int('price').notNull(), // store in cents
  active: boolean('active').default(true),
  createdAt: datetime('created_at').notNull(),
});

Querying

Drizzle’s query builder closely mirrors SQL while maintaining full type safety.

Basic CRUD

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

// SELECT
const allUsers = await db.select().from(users);

// SELECT with WHERE
const adminUsers = await db
  .select()
  .from(users)
  .where(eq(users.role, 'admin'));

// SELECT specific columns
const userNames = await db
  .select({ id: users.id, email: users.email })
  .from(users);

// INSERT
const [newUser] = await db
  .insert(users)
  .values({
    email: '[email protected]',
    name: 'Alice Johnson',
  })
  .returning();

// INSERT multiple
await db.insert(users).values([
  { email: '[email protected]', name: 'Bob' },
  { email: '[email protected]', name: 'Carol' },
]);

// UPDATE
await db
  .update(users)
  .set({ emailVerified: true, updatedAt: new Date() })
  .where(eq(users.id, userId));

// DELETE
await db.delete(posts).where(eq(posts.id, postId));

Complex Queries

// JOINs
const postsWithAuthors = await db
  .select({
    postId: posts.id,
    title: posts.title,
    authorName: users.name,
    authorEmail: users.email,
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .where(eq(posts.published, true))
  .orderBy(desc(posts.publishedAt))
  .limit(10);

// Compound WHERE
const recentPosts = await db
  .select()
  .from(posts)
  .where(
    and(
      eq(posts.published, true),
      gte(posts.publishedAt, new Date('2026-01-01')),
      or(
        like(posts.title, '%TypeScript%'),
        like(posts.title, '%JavaScript%')
      )
    )
  );

Relational Queries

Drizzle’s query API (built on top of the core API) provides Prisma-style include and with syntax:

// Define relations in 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],
  }),
}));
// Use relational API
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: [desc(posts.createdAt)],
      limit: 5,
    },
  },
  where: eq(users.emailVerified, true),
});
// Type: Array<User & { posts: Post[] }>

Migrations

Drizzle Kit handles migrations. Add a config file:

// drizzle.config.ts
import type { Config } from 'drizzle-kit';

export default {
  schema: './src/db/schema.ts',
  out: './drizzle/migrations',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
} satisfies Config;
# Generate migration files from schema changes
npx drizzle-kit generate

# Apply migrations to database
npx drizzle-kit migrate

# Push schema directly (dev only β€” no migration files)
npx drizzle-kit push

# Open Drizzle Studio
npx drizzle-kit studio

Generated migration files are plain SQL β€” no magic, easy to review:

-- drizzle/migrations/0001_add_email_verified.sql
ALTER TABLE "users" ADD COLUMN "email_verified" boolean DEFAULT false;

Row Level Security (RLS) Support

Drizzle 0.30+ adds first-class RLS support for PostgreSQL, critical for multi-tenant apps:

import { pgTable, pgPolicy, authenticatedRole } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';

export const documents = pgTable(
  'documents',
  {
    id: uuid('id').primaryKey().defaultRandom(),
    userId: uuid('user_id').notNull(),
    content: text('content'),
  },
  (table) => [
    pgPolicy('users_own_documents', {
      as: 'permissive',
      for: 'all',
      to: authenticatedRole,
      using: sql`${table.userId} = auth.uid()`,
    }),
  ]
);

This integrates cleanly with Supabase Row Level Security policies.


Drizzle Studio

Drizzle Studio is a database browser that runs locally against your schema:

npx drizzle-kit studio
# Opens at https://local.drizzle.studio

It gives you:

  • Table browser with filtering and sorting
  • Query editor with schema-aware autocomplete
  • Relation graph visualization
  • Read/write data directly (with confirmation for writes)

Performance vs. Prisma

The most common migration path in 2026 is Prisma β†’ Drizzle. Key differences:

AspectDrizzlePrisma
Bundle size~35 KB~2.5 MB (query engine)
Cold start~5ms~200-400ms
Query generationCompile-timeRuntime (Rust engine)
Schema syntaxTypeScriptPrisma SDL
Raw SQLdb.execute(sql\…`)`$queryRaw
Serverless supportExcellentImproved (Accelerate)
MigrationsDrizzle Kit (SQL)Prisma Migrate
ORM styleSQL-firstObject-first

For Cloudflare Workers and edge functions where cold starts are measured in milliseconds, Drizzle’s startup time is a decisive advantage.

Benchmark: Simple query on Neon Serverless

p50p99
Drizzle (neon-http)12ms28ms
Prisma (Accelerate)18ms45ms
Raw pg11ms25ms

PostgreSQL, MySQL, and SQLite Support

Drizzle has feature parity across all three:

// SQLite / Turso / Cloudflare D1
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';

const client = createClient({ url: process.env.TURSO_URL!, authToken: process.env.TURSO_TOKEN! });
export const db = drizzle(client);
// Cloudflare D1
import { drizzle } from 'drizzle-orm/d1';

export function createDb(d1: D1Database) {
  return drizzle(d1);
}

Transactions

await db.transaction(async (tx) => {
  const [order] = await tx
    .insert(orders)
    .values({ userId, total })
    .returning();

  await tx.insert(orderItems).values(
    items.map(item => ({ orderId: order.id, ...item }))
  );

  await tx
    .update(inventory)
    .set({ stock: sql`${inventory.stock} - 1` })
    .where(inArray(inventory.productId, items.map(i => i.productId)));
});

Best Practices

1. Export inferred types from your schema:

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

2. Create a db.ts singleton per environment:

// Don't create a new connection on every request
let _db: ReturnType<typeof drizzle> | null = null;
export function getDb() {
  if (!_db) _db = drizzle(client, { schema });
  return _db;
}

3. Use prepare() for hot-path queries:

const getUserById = db
  .select()
  .from(users)
  .where(eq(users.id, sql.placeholder('id')))
  .prepare('get_user_by_id');

// Reuse the prepared statement
const user = await getUserById.execute({ id: userId });

Getting Started

Drizzle ORM is the right choice if you:

  • Deploy to serverless or edge environments
  • Want SQL-level control with TypeScript types
  • Need fast cold starts
  • Work with multiple databases in the same project

Start with drizzle-kit push during development, graduate to migration files before production, and use Drizzle Studio for data exploration throughout.


Resources

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.