DevToolBoxGRATIS
Blog

Drizzle ORM Guide 2026: Type-Safe SQL, Schema, Migrations, Relations & Serverless

18 min readoleh DevToolBox
TL;DR

Drizzle ORM is a lightweight, type-safe TypeScript ORM that lets you write SQL-like queries with full autocompletion and compile-time type checking. It supports PostgreSQL, MySQL, and SQLite with zero runtime overhead. Define schemas in TypeScript, run migrations with drizzle-kit, browse data with Drizzle Studio, and deploy to serverless environments with minimal bundle size. Drizzle gives you the power of raw SQL with the safety of TypeScript.

What Is Drizzle ORM?

Drizzle ORM is a TypeScript ORM that takes a fundamentally different approach from traditional ORMs. Instead of abstracting SQL away behind method chains, Drizzle embraces SQL syntax and maps it directly to TypeScript. Every query you write looks like SQL but has full type inference, autocompletion, and compile-time validation. There is no query builder magic or hidden runtime overhead.

Why Choose Drizzle?

  • SQL-like syntax: queries read like SQL, not method chains
  • Type-safe: full TypeScript inference from schema to query results
  • Lightweight: zero dependencies, minimal bundle size (~7.4KB gzipped)
  • Serverless-ready: no connection pooling issues, works with edge runtimes
  • Multi-database: PostgreSQL, MySQL, SQLite with the same API
  • Drizzle Kit: schema migrations, push, and introspection from CLI
  • Drizzle Studio: browser-based database GUI for viewing and editing data

Drizzle vs Prisma vs TypeORM

Each ORM makes different trade-offs. Drizzle prioritizes SQL familiarity and minimal overhead. Prisma prioritizes developer experience with its schema language. TypeORM follows traditional ORM patterns with decorators.

FeatureDrizzlePrismaTypeORM
Schema languageTypeScriptPrisma Schema (.prisma)TypeScript decorators
Query styleSQL-likeMethod chainingRepository / QueryBuilder
Type safetyFull (inferred from schema)Full (generated client)Partial (decorators)
Bundle size~7.4KB gzipped~2MB (Prisma Client)~1.5MB
DependenciesZeroPrisma Engine (Rust)Many (reflect-metadata, etc.)
DatabasesPostgreSQL, MySQL, SQLitePostgreSQL, MySQL, SQLite, MongoDB, SQL ServerPostgreSQL, MySQL, SQLite, many more
ServerlessExcellent (minimal overhead)Good (engine binary needed)Poor (heavy startup)
Migrationsdrizzle-kit generate/pushprisma migrateTypeORM CLI
GUI ToolDrizzle StudioPrisma StudioNone built-in
Raw SQLFirst-class (sql`` tag)prisma.$queryRawquery()

Schema Definition

Drizzle schemas are written in plain TypeScript files. Each table is defined using helper functions that map directly to SQL column types. The schema serves as both your database definition and your TypeScript types.

PostgreSQL Schema

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

// Define an enum
export const roleEnum = pgEnum("role", ["admin", "user", "guest"]);

// Users table
export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 255 }).notNull(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  role: roleEnum("role").default("user").notNull(),
  bio: text("bio"),
  isActive: boolean("is_active").default(true).notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
}, (table) => ({
  emailIdx: uniqueIndex("email_idx").on(table.email),
}));

// Posts table
export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 500 }).notNull(),
  content: text("content").notNull(),
  published: boolean("published").default(false).notNull(),
  authorId: integer("author_id").notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

// Comments table
export const comments = pgTable("comments", {
  id: serial("id").primaryKey(),
  body: text("body").notNull(),
  postId: integer("post_id").notNull()
    .references(() => posts.id, { onDelete: "cascade" }),
  authorId: integer("author_id").notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

MySQL Schema

// src/db/schema.ts (MySQL)
import {
  mysqlTable, serial, varchar, text,
  int, timestamp, boolean, mysqlEnum
} from "drizzle-orm/mysql-core";

export const users = mysqlTable("users", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 255 }).notNull(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  role: mysqlEnum("role", ["admin", "user", "guest"])
    .default("user").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

export const posts = mysqlTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 500 }).notNull(),
  content: text("content").notNull(),
  authorId: int("author_id").notNull()
    .references(() => users.id),
});

SQLite Schema

// src/db/schema.ts (SQLite)
import {
  sqliteTable, integer, text
} from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  role: text("role", { enum: ["admin", "user", "guest"] })
    .default("user").notNull(),
  createdAt: text("created_at")
    .default("CURRENT_TIMESTAMP").notNull(),
});

export const posts = sqliteTable("posts", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  content: text("content").notNull(),
  authorId: integer("author_id").notNull()
    .references(() => users.id),
});

Project Setup and Configuration

Setting up Drizzle requires installing the ORM package, a database driver, and drizzle-kit for migrations. The configuration is minimal compared to other ORMs.

# Install Drizzle ORM + PostgreSQL driver
npm install drizzle-orm postgres
npm install -D drizzle-kit

# Or with MySQL
npm install drizzle-orm mysql2
npm install -D drizzle-kit

# Or with SQLite
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
// src/db/index.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

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

// For MySQL:
// import { drizzle } from "drizzle-orm/mysql2";
// import mysql from "mysql2/promise";
// const pool = mysql.createPool(process.env.DATABASE_URL!);
// export const db = drizzle(pool, { schema });

// For SQLite:
// import { drizzle } from "drizzle-orm/better-sqlite3";
// import Database from "better-sqlite3";
// const sqlite = new Database("local.db");
// export const db = drizzle(sqlite, { schema });

Type-Safe Queries

Drizzle provides two query APIs: the SQL-like core API and the relational query API. The core API mirrors SQL syntax directly. The relational API provides a more declarative way to fetch nested data.

SELECT Queries

import { eq, gt, like, and, or, desc, asc, count, sql } from "drizzle-orm";
import { db } from "./db";
import { users, posts } from "./db/schema";

// Select all users
const allUsers = await db.select().from(users);
// Type: { id: number; name: string; email: string; role: string; ... }[]

// Select specific columns
const userNames = await db.select({
  id: users.id,
  name: users.name,
}).from(users);
// Type: { id: number; name: string }[]

// WHERE clause with conditions
const activeAdmins = await db.select()
  .from(users)
  .where(
    and(
      eq(users.role, "admin"),
      eq(users.isActive, true)
    )
  );

// Complex filtering
const searchResults = await db.select()
  .from(users)
  .where(
    or(
      like(users.name, "%alice%"),
      like(users.email, "%alice%")
    )
  )
  .orderBy(desc(users.createdAt))
  .limit(10)
  .offset(0);

// Aggregation
const userCount = await db.select({
  role: users.role,
  total: count(),
}).from(users).groupBy(users.role);
// Type: { role: string; total: number }[]

INSERT Operations

// Insert a single row
const newUser = await db.insert(users).values({
  name: "Alice",
  email: "alice@example.com",
  role: "admin",
}).returning();
// Returns: { id: 1, name: "Alice", ... }[]

// Insert multiple rows
await db.insert(users).values([
  { name: "Bob", email: "bob@example.com" },
  { name: "Charlie", email: "charlie@example.com" },
]);

// Upsert (insert or update on conflict)
await db.insert(users)
  .values({ name: "Alice", email: "alice@example.com" })
  .onConflictDoUpdate({
    target: users.email,
    set: { name: "Alice Updated" },
  });

UPDATE Operations

// Update rows matching a condition
await db.update(users)
  .set({
    role: "admin",
    updatedAt: new Date(),
  })
  .where(eq(users.email, "alice@example.com"));

// Update with returning
const updated = await db.update(users)
  .set({ isActive: false })
  .where(eq(users.id, 1))
  .returning({ id: users.id, name: users.name });

DELETE Operations

// Delete rows matching a condition
await db.delete(users)
  .where(eq(users.id, 1));

// Delete with returning
const deleted = await db.delete(posts)
  .where(
    and(
      eq(posts.published, false),
      gt(posts.createdAt, new Date("2024-01-01"))
    )
  )
  .returning();

// Delete all rows (use with caution)
// await db.delete(users);

Relations and Joins

Drizzle supports both explicit SQL joins and a relational query API for nested data fetching. Relations are defined separately from the schema, keeping your table definitions clean.

Defining Relations

// src/db/relations.ts
import { relations } from "drizzle-orm";
import { users, posts, comments } from "./schema";

// User has many posts and comments
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
  comments: many(comments),
}));

// Post belongs to user, has many comments
export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
  comments: many(comments),
}));

// Comment belongs to post and user
export const commentsRelations = relations(comments, ({ one }) => ({
  post: one(posts, {
    fields: [comments.postId],
    references: [posts.id],
  }),
  author: one(users, {
    fields: [comments.authorId],
    references: [users.id],
  }),
}));

SQL Joins

// Inner join: users with their posts
const usersWithPosts = await db.select({
  userId: users.id,
  userName: users.name,
  postTitle: posts.title,
  postCreated: posts.createdAt,
}).from(users)
  .innerJoin(posts, eq(users.id, posts.authorId))
  .where(eq(posts.published, true));

// Left join: all users, including those without posts
const allUsersWithPosts = await db.select({
  userId: users.id,
  userName: users.name,
  postTitle: posts.title,
}).from(users)
  .leftJoin(posts, eq(users.id, posts.authorId));

// Multi-table join
const postWithComments = await db.select({
  postTitle: posts.title,
  commentBody: comments.body,
  authorName: users.name,
}).from(posts)
  .innerJoin(comments, eq(posts.id, comments.postId))
  .innerJoin(users, eq(comments.authorId, users.id))
  .where(eq(posts.id, 1));

Relational Queries

// Fetch user with their posts and comments (nested)
const userWithData = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: [desc(posts.createdAt)],
      limit: 5,
      with: {
        comments: {
          with: {
            author: true,
          },
        },
      },
    },
  },
});
// Result type is fully inferred:
// {
//   id: number; name: string; email: string;
//   posts: {
//     id: number; title: string;
//     comments: { body: string; author: { name: string } }[]
//   }[]
// }

// Find many with filtering
const recentPosts = await db.query.posts.findMany({
  where: eq(posts.published, true),
  orderBy: [desc(posts.createdAt)],
  limit: 20,
  with: {
    author: {
      columns: { id: true, name: true },
    },
  },
});

Migrations with drizzle-kit

Drizzle Kit generates SQL migration files by comparing your TypeScript schema against the current database state. It supports generate, push, and introspect workflows.

Generating Migrations

# Generate a migration from schema changes
npx drizzle-kit generate

# Output: drizzle/0001_cool_migration.sql
# CREATE TABLE "users" (
#   "id" serial PRIMARY KEY NOT NULL,
#   "name" varchar(255) NOT NULL,
#   "email" varchar(255) NOT NULL,
#   ...
# );

# Apply migrations programmatically
# In your application entry point:
# import { migrate } from "drizzle-orm/postgres-js/migrator";
# await migrate(db, { migrationsFolder: "./drizzle" });

Push (Development)

# Push schema directly to database (development only)
# Skips migration files, applies changes immediately
npx drizzle-kit push

# Check what changes would be applied
npx drizzle-kit check

Introspect Existing Database

# Generate Drizzle schema from existing database
npx drizzle-kit introspect

# Output: drizzle/schema.ts
# This creates TypeScript schema files that match
# your existing database structure.
# Useful for migrating from another ORM or raw SQL.

Drizzle Studio

Drizzle Studio is a browser-based database GUI that comes built into drizzle-kit. Run one command and get a visual interface for browsing tables, editing rows, running queries, and inspecting your schema. It connects directly to your database using the same drizzle.config.ts configuration.

# Launch Drizzle Studio
npx drizzle-kit studio

# Opens a browser GUI at https://local.drizzle.studio
# Features:
# - Browse all tables and their data
# - Edit rows inline
# - Run custom SQL queries
# - View table schemas and relations
# - Filter and sort data
# - Export query results
#
# Uses the same drizzle.config.ts for DB connection.
# No additional setup required.

Framework Integration

Drizzle works with any TypeScript runtime. Here are patterns for the most popular frameworks.

Next.js Integration

// src/db/index.ts (Next.js with Neon serverless)
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";

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

// app/users/page.tsx (Server Component)
import { db } from "@/db";
import { users } from "@/db/schema";
import { desc } from "drizzle-orm";

export default async function UsersPage() {
  const allUsers = await db.select()
    .from(users)
    .orderBy(desc(users.createdAt))
    .limit(50);

  return (
    <ul>
      {allUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// app/api/users/route.ts (Route Handler)
import { NextResponse } from "next/server";
import { db } from "@/db";
import { users } from "@/db/schema";

export async function POST(request: Request) {
  const body = await request.json();
  const newUser = await db.insert(users)
    .values(body)
    .returning();
  return NextResponse.json(newUser[0]);
}

Hono Integration

// src/index.ts (Hono with Drizzle)
import { Hono } from "hono";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { users } from "./db/schema";
import { eq } from "drizzle-orm";

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

const app = new Hono();

app.get("/users", async (c) => {
  const allUsers = await db.select().from(users);
  return c.json(allUsers);
});

app.get("/users/:id", async (c) => {
  const id = Number(c.req.param("id"));
  const user = await db.select().from(users)
    .where(eq(users.id, id));
  if (!user.length) return c.json({ error: "Not found" }, 404);
  return c.json(user[0]);
});

export default app;

Serverless and Edge

// Cloudflare Workers with D1 (SQLite)
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 allUsers = await db.select().from(schema.users);
    return Response.json(allUsers);
  },
};

// Vercel Edge with Neon
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";

export const runtime = "edge";

export async function GET() {
  const sql = neon(process.env.DATABASE_URL!);
  const db = drizzle(sql);
  const result = await db.select().from(users);
  return Response.json(result);
}

// AWS Lambda with PlanetScale
import { drizzle } from "drizzle-orm/planetscale-serverless";
import { Client } from "@planetscale/database";

const client = new Client({ url: process.env.DATABASE_URL! });
const db = drizzle(client);

Transactions and Prepared Statements

Drizzle supports database transactions for atomic operations and prepared statements for repeated queries with better performance.

Using Transactions

// Basic transaction
await db.transaction(async (tx) => {
  // All operations inside share the same transaction
  const [newUser] = await tx.insert(users)
    .values({ name: "Alice", email: "alice@example.com" })
    .returning();

  await tx.insert(posts).values({
    title: "First Post",
    content: "Hello world!",
    authorId: newUser.id,
  });

  // If any operation fails, all changes are rolled back
});

// Nested transaction with savepoints
await db.transaction(async (tx) => {
  await tx.insert(users)
    .values({ name: "Bob", email: "bob@example.com" });

  try {
    await tx.transaction(async (nestedTx) => {
      await nestedTx.insert(posts).values({
        title: "Draft",
        content: "...",
        authorId: 999, // invalid FK
      });
    });
  } catch (e) {
    // Nested transaction rolled back,
    // outer transaction continues
    console.log("Nested transaction failed:", e);
  }
  // Bob is still inserted
});

Prepared Statements

import { sql, placeholder } from "drizzle-orm";

// Prepared statement with placeholder
const getUserById = db.select()
  .from(users)
  .where(eq(users.id, placeholder("id")))
  .prepare("get_user_by_id");

// Execute with different values (reuses query plan)
const user1 = await getUserById.execute({ id: 1 });
const user2 = await getUserById.execute({ id: 2 });

// Prepared statement with multiple placeholders
const searchUsers = db.select()
  .from(users)
  .where(
    and(
      eq(users.role, placeholder("role")),
      eq(users.isActive, placeholder("active"))
    )
  )
  .limit(placeholder("limit"))
  .prepare("search_users");

const admins = await searchUsers.execute({
  role: "admin",
  active: true,
  limit: 10,
});

// Raw SQL when needed
const result = await db.execute(
  sql`SELECT * FROM users WHERE name ILIKE ${"%" + search + "%"}`
);

Performance Optimization

Drizzle is already one of the fastest TypeScript ORMs due to its thin abstraction layer. These patterns help you get even better performance in production.

  • Use select() with specific columns instead of select() with all columns to reduce data transfer
  • Create database indexes on columns used in WHERE, ORDER BY, and JOIN conditions
  • Use prepared statements for frequently executed queries to skip query planning
  • Batch related writes in transactions to reduce round trips
  • Use connection pooling (pg Pool, PlanetScale serverless driver) for serverless
  • Enable query logging in development to catch N+1 queries early
  • Use the relational query API for nested data instead of multiple sequential queries
  • Add .limit() to all queries that return lists to prevent unbounded result sets
// Performance patterns

// 1. Select only needed columns
const names = await db.select({
  id: users.id,
  name: users.name,
}).from(users); // faster than db.select().from(users)

// 2. Add indexes in schema
import { index } from "drizzle-orm/pg-core";

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 500 }).notNull(),
  authorId: integer("author_id").notNull(),
  published: boolean("published").default(false),
  createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
  authorIdx: index("author_idx").on(table.authorId),
  publishedIdx: index("published_idx")
    .on(table.published, table.createdAt),
}));

// 3. Batch operations in transactions
await db.transaction(async (tx) => {
  await tx.insert(posts).values(batchOfPosts);
  await tx.update(users)
    .set({ updatedAt: new Date() })
    .where(eq(users.id, authorId));
});

// 4. Always use .limit()
const recent = await db.select().from(posts)
  .orderBy(desc(posts.createdAt))
  .limit(20);  // never fetch unbounded result sets

// 5. Enable query logging
const db = drizzle(connection, {
  schema,
  logger: true,  // logs all queries to console
});

Frequently Asked Questions

How does Drizzle ORM compare to Prisma?

Drizzle uses TypeScript for schema definition and SQL-like query syntax, while Prisma uses its own schema language (schema.prisma) and a method-chain API. Drizzle has zero dependencies and a smaller bundle size (~7.4KB vs ~2MB for Prisma Client). Drizzle gives you more control over the generated SQL, while Prisma provides a higher level of abstraction. Drizzle works better in serverless and edge environments due to its minimal footprint.

Does Drizzle support MongoDB or other NoSQL databases?

No. Drizzle ORM is designed exclusively for SQL databases: PostgreSQL, MySQL, and SQLite. It embraces SQL rather than abstracting it away. For MongoDB, consider Mongoose or Prisma which supports MongoDB.

Can I use Drizzle with an existing database?

Yes. Use drizzle-kit introspect to generate TypeScript schema files from an existing database. The command reads your database structure and creates matching Drizzle schema definitions that you can use immediately.

Is Drizzle production-ready?

Yes. Drizzle ORM is used in production by many companies and has a stable v0.30+ API. It is actively maintained, has comprehensive TypeScript types, and its thin abstraction layer means fewer potential bugs between your code and the database.

How do I handle database migrations in production?

Use drizzle-kit generate to create SQL migration files, then run them with drizzle-kit migrate or use the migrate() function in your application code during startup. Store migration files in version control and apply them in CI/CD pipelines.

Does Drizzle work with serverless platforms?

Yes. Drizzle is ideal for serverless environments because of its minimal bundle size and zero dependencies. It works with Vercel Edge Functions, Cloudflare Workers, AWS Lambda, and Deno Deploy. Use serverless-compatible drivers like @neondatabase/serverless, @planetscale/database, or better-sqlite3.

What is Drizzle Studio and how do I use it?

Drizzle Studio is a browser-based database GUI included with drizzle-kit. Run npx drizzle-kit studio to start it. It provides a visual interface for browsing tables, editing data, running queries, and inspecting your schema. It uses the same drizzle.config.ts connection settings.

How do I handle relations in Drizzle?

Drizzle provides two approaches. For explicit joins, use the SQL-like leftJoin, innerJoin, and fullJoin methods. For nested data fetching, define relations using the relations() function and use the relational query API (db.query.tableName.findMany) with the with option to include related data.

Key Takeaways
  • Drizzle ORM provides SQL-like TypeScript queries with full type inference and zero runtime overhead
  • Supports PostgreSQL, MySQL, and SQLite with consistent APIs and database-specific features
  • Schema definitions in TypeScript double as both database DDL and application types
  • drizzle-kit handles migrations (generate, push, introspect) and includes Drizzle Studio for browsing data
  • Minimal bundle size (~7.4KB) makes Drizzle ideal for serverless and edge deployments
  • Relations support both SQL joins and a declarative relational query API for nested data
  • Transactions, prepared statements, and selective column queries optimize production performance
𝕏 Twitterin LinkedIn
Apakah ini membantu?

Tetap Update

Dapatkan tips dev mingguan dan tool baru.

Tanpa spam. Berhenti kapan saja.

Coba Alat Terkait

{ }JSON FormatterTSJSON to TypeScriptSQLSQL Formatter

Artikel Terkait

Panduan Prisma Schema dan Relasi

Kuasai desain schema Prisma: model, relasi, enum, indeks, dan migrasi.

Prisma vs Drizzle vs TypeORM

Perbandingan ORM TypeScript.

Next.js Advanced Guide: App Router, Server Components, Data Fetching, Middleware & Performance

Complete Next.js advanced guide covering App Router architecture, React Server Components, streaming SSR, data fetching patterns, middleware, route handlers, parallel and intercepting routes, caching strategies, ISR, image optimization, and deployment best practices.