Drizzle ORM: The TypeScript ORM Taking Over
Drizzle ORM has quickly become the go‑to data layer for TypeScript developers who crave both type‑safety and raw performance. Unlike traditional ORMs that hide SQL behind opaque abstractions, Drizzle lets you write expressive, type‑checked queries while still feeling like you’re working directly with the database. In this article we’ll explore why Drizzle is gaining traction, walk through a couple of hands‑on examples, and share some pro tips that will help you get the most out of this modern ORM.
Why Drizzle Stands Out
First and foremost, Drizzle embraces TypeScript’s type system from the ground up. Every table, column, and query returns a fully inferred type, so you catch mismatched fields at compile time instead of at runtime. This eliminates a whole class of bugs that plague JavaScript‑only ORMs.
Performance is another badge of honor. Drizzle generates lean SQL strings without the heavy runtime overhead that many “feature‑rich” ORMs carry. The result is faster query execution and lower memory footprints—critical for serverless functions and high‑traffic APIs.
Finally, Drizzle adopts a “zero‑magic” philosophy. You define tables using plain objects, and the library never mutates them behind the scenes. This makes the codebase easier to reason about, especially when onboarding new developers.
Getting Started: Installation & Basic Setup
Before diving into code, let’s get Drizzle installed in a fresh Node.js project. Open your terminal and run:
npm install drizzle-orm @drizzle/orm sqlite3
# or, if you prefer Yarn
yarn add drizzle-orm @drizzle/orm sqlite3
For this guide we’ll use SQLite as the backing store because it requires no external server. In a production setting you’d swap it out for PostgreSQL, MySQL, or any supported driver.
Next, create a db.ts file that bootstraps the connection and defines a simple users table.
import { drizzle } from 'drizzle-orm';
import { sqlite } from '@drizzle/orm/sqlite';
import Database from 'better-sqlite3';
// Initialize the SQLite connection
const sqliteDb = new Database('dev.db');
export const db = drizzle(sqliteDb, { schema: {} });
// Define the Users table
export const users = db.table('users', {
id: db.int('id').primaryKey().autoIncrement(),
email: db.text('email').unique(),
name: db.text('name'),
createdAt: db.timestamp('created_at').defaultNow(),
});
Notice how each column declaration returns a strongly typed object. The users constant now carries full type information for every field.
Creating Records with Full Type Safety
Inserting data is as simple as calling insert on the table reference. Drizzle will infer the required shape of the payload, and any stray property will raise a TypeScript error.
import { db, users } from './db';
// Insert a new user
await db.insert(users).values({
email: 'alice@example.com',
name: 'Alice',
// createdAt is optional because of defaultNow()
});
If you accidentally misspell a column name, the compiler will scream:
// ❌ TypeScript error: Object literal may only specify known properties
await db.insert(users).values({
emai: 'bob@example.com', // typo!
name: 'Bob',
});
This immediate feedback is a game‑changer for large teams where schema drift is a common pain point.
Reading Data: Typed Queries Made Easy
Fetching rows follows the same pattern. Drizzle offers a fluent API that mirrors SQL’s SELECT syntax while preserving type inference.
import { eq } from 'drizzle-orm/expressions';
// Get a user by email
const alice = await db
.select()
.from(users)
.where(eq(users.email, 'alice@example.com'))
.limit(1)
.then(rows => rows[0]);
// alice is inferred as:
// {
// id: number;
// email: string;
// name: string | null;
// createdAt: Date;
// }
Because the result type is derived from the users schema, you get autocomplete for every column in your IDE. No more “any” objects floating around.
Complex Queries: Joins, Aggregations, and Subqueries
Real‑world applications rarely stick to single‑table selects. Drizzle’s query builder scales gracefully to multi‑table joins and aggregations, all while staying type‑safe.
Suppose we have a posts table that references users. First, define the new table:
export const posts = db.table('posts', {
id: db.int('id').primaryKey().autoIncrement(),
authorId: db.int('author_id').references(() => users.id),
title: db.text('title'),
content: db.text('content'),
publishedAt: db.timestamp('published_at').nullable(),
});
Now, fetch the latest three posts along with the author’s name using a join:
import { desc } from 'drizzle-orm/order';
const recentPosts = await db
.select({
postId: posts.id,
title: posts.title,
authorName: users.name,
publishedAt: posts.publishedAt,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.orderBy(desc(posts.publishedAt))
.limit(3);
The resulting recentPosts array has a type that combines fields from both tables, giving you full autocomplete on authorName and publishedAt.
Transactions: Keeping Data Consistent
When you need atomic operations—say, creating a post and updating the author’s post count—Drizzle’s transaction API makes it painless.
await db.transaction(async (tx) => {
// Insert the post
const [postId] = await tx.insert(posts).values({
authorId: 1,
title: 'Understanding Drizzle ORM',
content: '...',
});
// Increment the author’s post counter (hypothetical column)
await tx
.update(users)
.set({ postCount: db.raw('postCount + 1') })
.where(eq(users.id, 1));
});
If any step throws, the whole transaction rolls back, guaranteeing data integrity without manual cleanup.
Migrations: Evolving Your Schema Safely
Drizzle ships with a lightweight migration tool that leverages the same schema definitions you already wrote. Create a migrations folder and add a file named 2024-09-01-add-posts-table.ts:
import { migration } from 'drizzle-orm/migration';
import { posts } from '../db';
export default migration(async (db) => {
await db.schema.createTable(posts);
});
Run migrations with a single CLI command:
npx drizzle-kit migrate
This approach keeps your schema source of truth in code, eliminating the drift that often occurs when SQL files and application models diverge.
Real‑World Use Cases
1. Serverless APIs
Serverless functions thrive on low cold‑start times and minimal bundle size. Drizzle’s tiny runtime (< 20 KB gzipped) and its ability to generate raw SQL strings on the fly make it an ideal fit for AWS Lambda, Vercel Edge Functions, or Cloudflare Workers.
2. Micro‑service Data Layers
In a micro‑service architecture each service often owns its own database. Drizzle’s schema‑first design means you can generate TypeScript types from a single source, then share those types across services via a monorepo or a private npm package. This guarantees consistency without a heavyweight ORM.
3. Real‑time Dashboards
When building dashboards that poll data every few seconds, query latency matters. Drizzle’s lean SQL generation combined with prepared‑statement caching (when using drivers that support it) yields sub‑millisecond response times, keeping UI updates buttery smooth.
Pro tip: Pair Drizzle with sql-template-strings for raw queries that still benefit from TypeScript inference. This hybrid approach gives you the flexibility of hand‑crafted SQL while retaining compile‑time safety.
Advanced Patterns: Soft Deletes & Auditing
Many applications require soft deletes (marking rows as inactive) rather than physical removal. Drizzle makes this pattern straightforward by extending the schema with a deletedAt column and building a reusable query helper.
export const softDeletable = (table) => ({
delete: (id) =>
db.update(table).set({ deletedAt: db.raw('CURRENT_TIMESTAMP') }).where(eq(table.id, id)),
findActive: () => db.select().from(table).where(eq(table.deletedAt, null)),
});
Usage with the users table:
const userOps = softDeletable(users);
// Soft‑delete user #5
await userOps.delete(5);
// Fetch only active users
const activeUsers = await userOps.findActive();
The helper returns fully typed methods, so you never lose autocomplete even when abstracting common patterns.
Testing with Drizzle: In‑Memory Databases
Writing unit tests that hit a real database can be slow and flaky. Drizzle works seamlessly with in‑memory SQLite, allowing you to spin up a fresh database for each test suite.
import { drizzle } from 'drizzle-orm';
import { sqlite } from '@drizzle/orm/sqlite';
import Database from 'better-sqlite3';
import { users } from '../db';
let testDb;
beforeAll(() => {
const memoryDb = new Database(':memory:');
testDb = drizzle(memoryDb, { schema: {} });
// Run migrations or create tables directly
await testDb.schema.createTable(users);
});
test('creates a user', async () => {
await testDb.insert(users).values({ email: 'test@demo.com', name: 'Test' });
const result = await testDb.select().from(users).where(eq(users.email, 'test@demo.com'));
expect(result).toHaveLength(1);
});
This setup ensures isolation between tests and runs in a fraction of a second compared to a Dockerized PostgreSQL instance.
Performance Benchmarks: Drizzle vs. Competitors
Recent community benchmarks (2024‑09) compare Drizzle against Prisma and TypeORM on a typical CRUD workload:
- Insert latency: Drizzle – 1.2 ms, Prisma – 3.8 ms, TypeORM – 4.5 ms
- Select latency (single row): Drizzle – 0.9 ms, Prisma – 2.4 ms, TypeORM – 2.9 ms
- Memory footprint (cold start): Drizzle – 12 MB, Prisma – 45 MB, TypeORM – 38 MB
These numbers illustrate why Drizzle is a natural fit for latency‑sensitive environments like edge computing and high‑throughput APIs.
Common Pitfalls & How to Avoid Them
1. Over‑using raw SQL. While Drizzle allows raw queries, mixing them indiscriminately defeats the purpose of type safety. Reserve raw SQL for performance‑critical paths that the query builder can’t express.
2. Ignoring migrations. Because Drizzle’s schema lives in code, it’s tempting to modify tables without generating migration files. Always run drizzle-kit migrate after schema changes to keep production in sync.
3. Forgetting to close connections. In long‑running services you should gracefully shut down the database client to avoid connection leaks. Drizzle exposes a close() method on the underlying driver for this purpose.
Pro tip: Use drizzle-kit generate:types to emit a separate .d.ts file for your schema. This is especially handy when sharing types across multiple packages in a monorepo.
Integrating Drizzle with Next.js API Routes
Next.js developers love the file‑based routing model, and Drizzle fits right in. Create a lib/db.ts that exports a singleton instance, then import it inside your API route.
// lib/db.ts
import { drizzle } from 'drizzle-orm';
import { sqlite } from '@drizzle/orm/sqlite';
import Database from 'better-sqlite3';
import { users } from './schema';
const sqliteDb = new Database('next.db');
export const db = drizzle(sqliteDb, { schema: { users } });
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { db, users } from '../../lib/db';
import { eq } from 'drizzle-orm/expressions';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
const allUsers = await db.select().from(users);
res.status(200).json(allUsers);
} else if (req.method === 'POST') {
const { email, name } = req.body;
const [id] = await db.insert(users).values({ email, name });
res.status(201).json({ id });
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Because the route handler is fully typed, any misuse of the request payload (e.g., missing email) will be caught during development.
Future Roadmap: What’s Coming Next?
Drizzle’s maintainers have an ambitious roadmap that includes built‑in support for GraphQL resolvers, automatic pagination helpers, and tighter integration with Prisma’s schema introspection tools. Keeping an eye on the GitHub repo will help you adopt new features early.
Community contributions are also encouraged. The project’s modular architecture makes it easy to write custom plugins—for example, a plugin that logs every generated SQL statement to a centralized observability platform.
Conclusion
Drizzle ORM delivers the perfect blend of type safety, performance, and developer ergonomics that modern TypeScript applications demand. By defining schemas in plain objects, offering a fluent yet lightweight query builder, and providing a robust migration system, it solves many pain points left by older ORMs. Whether you’re building serverless APIs, micro‑services, or real‑time dashboards, Drizzle’s “zero‑magic” approach ensures you stay in control of your SQL while still enjoying the comforts of a modern data layer.
Start experimenting today, and you’ll quickly see why the TypeScript community is rallying around Drizzle as the ORM of choice for the next generation of web applications.