Frameworks

Drizzle ORM: The Lightweight TypeScript ORM That Developers Actually Enjoy Using

Drizzle ORM: The Lightweight TypeScript ORM That Developers Actually Enjoy Using

If you have spent any meaningful time building TypeScript applications that interact with databases, you have likely encountered the uncomfortable tension that exists at the heart of every ORM. On one side, you have tools like Prisma that offer excellent developer experience and type safety but introduce a heavy runtime, a custom schema language, and performance overhead that can become painful at scale. On the other side, you have raw SQL query builders like Knex that give you complete control but leave you writing fragile, untyped queries that break silently when your schema changes. Drizzle ORM exists precisely in the gap between these two extremes, and it has rapidly become one of the most talked-about database tools in the TypeScript ecosystem.

Drizzle ORM is a TypeScript-first ORM that takes a radically different approach from its predecessors. Instead of inventing a custom schema language or generating heavy client code, Drizzle lets you define your database schema using plain TypeScript files. Your schema definitions are your source of truth, and the entire query API is derived from them at compile time. The result is a tool that provides full type safety across your entire data layer with near-zero runtime overhead, a SQL-like query syntax that experienced developers can read without consulting documentation, and a migration system that generates standard SQL files you can inspect, modify, and version control like any other code.

Since its first stable release in 2023, Drizzle has accumulated over 25,000 GitHub stars and has been adopted by teams building everything from small side projects to production applications handling millions of requests per day. It supports PostgreSQL, MySQL, SQLite, and several serverless database providers. In this guide, we will examine what makes Drizzle ORM different, walk through practical implementation patterns, and explore why so many developers are choosing it over more established alternatives.

Why Drizzle ORM Exists: The Problem with Existing Solutions

To understand why Drizzle has gained traction so quickly, you need to understand the fundamental problems that TypeScript developers face when working with databases. Every existing ORM forces you to make compromises, and those compromises accumulate over time into genuine engineering pain.

Traditional ORMs like TypeORM and Sequelize were designed before TypeScript achieved mainstream adoption. They bolt type safety onto an architecture that was fundamentally designed for JavaScript, and the result is type definitions that are incomplete, unreliable, or so complex that they confuse the TypeScript compiler. If you have ever seen a TypeORM query that compiles without errors but returns undefined at runtime because a relation was not eagerly loaded, you know exactly how frustrating this can be.

Prisma addressed many of these problems by building type safety into its core architecture. It generates a typed client from a .prisma schema file, and the generated types are remarkably comprehensive. However, Prisma introduces its own set of tradeoffs. The Prisma Client is a generated artifact that must be regenerated every time the schema changes. The query engine is a Rust binary that runs as a separate process, adding latency and memory overhead. The Prisma schema language (.prisma files) is a custom DSL that you must learn in addition to SQL and TypeScript. And Prisma’s query API, while powerful, deliberately abstracts away SQL in ways that can make complex queries difficult to express and difficult to optimize. When you need to write a query that does not fit neatly into Prisma’s model, you are forced to drop down to raw SQL — and at that point, you lose all the type safety that was Prisma’s primary selling point.

Drizzle takes a fundamentally different architectural approach. There is no code generation step, no custom schema language, no separate query engine, and no heavy runtime. Your schema is TypeScript. Your queries are TypeScript. The types flow directly from your schema definitions through the query builder to your application code, all resolved at compile time. The runtime layer is a thin wrapper around your database driver that does almost nothing except translate your type-safe query objects into SQL strings and hand them to the driver.

Defining Your Schema: TypeScript as the Source of Truth

The foundation of every Drizzle project is the schema definition. Unlike Prisma’s custom .prisma files or TypeORM’s decorator-heavy entity classes, Drizzle schemas are plain TypeScript modules that use a declarative API to describe your tables, columns, relationships, and constraints. This approach has a profound implication: your schema definitions are just code, which means they can be composed, abstracted, tested, and refactored using all the same tools and patterns you use for the rest of your application.

Here is a practical example that demonstrates how to define a schema for a project management application — the kind of schema you might use if you were building a tool similar to Taskee, which handles exactly these types of task and project relationships at scale:

// schema.ts — Drizzle ORM schema definition
// Plain TypeScript, no code generation, no custom DSL

import { pgTable, serial, text, varchar, integer,
         timestamp, boolean, pgEnum, index,
         uniqueIndex, primaryKey } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

// Enums are defined as PostgreSQL native enums
export const projectStatusEnum = pgEnum('project_status', [
  'planning', 'active', 'paused', 'completed', 'archived'
]);

export const taskPriorityEnum = pgEnum('task_priority', [
  'low', 'medium', 'high', 'critical'
]);

// Users table
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  displayName: varchar('display_name', { length: 100 }).notNull(),
  avatarUrl: text('avatar_url'),
  role: varchar('role', { length: 20 }).default('member').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
  // Indexes are defined alongside the table
  emailIdx: uniqueIndex('users_email_idx').on(table.email),
}));

// Projects table
export const projects = pgTable('projects', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 200 }).notNull(),
  description: text('description'),
  status: projectStatusEnum('status').default('planning').notNull(),
  ownerId: integer('owner_id').references(() => users.id).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  deadline: timestamp('deadline'),
}, (table) => ({
  ownerIdx: index('projects_owner_idx').on(table.ownerId),
  statusIdx: index('projects_status_idx').on(table.status),
}));

// Tasks table
export const tasks = pgTable('tasks', {
  id: serial('id').primaryKey(),
  title: varchar('title', { length: 300 }).notNull(),
  description: text('description'),
  priority: taskPriorityEnum('priority').default('medium').notNull(),
  isCompleted: boolean('is_completed').default(false).notNull(),
  projectId: integer('project_id').references(() => projects.id).notNull(),
  assigneeId: integer('assignee_id').references(() => users.id),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  dueDate: timestamp('due_date'),
}, (table) => ({
  projectIdx: index('tasks_project_idx').on(table.projectId),
  assigneeIdx: index('tasks_assignee_idx').on(table.assigneeId),
  priorityIdx: index('tasks_priority_idx').on(table.priority),
}));

// Tags table and many-to-many join table
export const tags = pgTable('tags', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 50 }).notNull().unique(),
  color: varchar('color', { length: 7 }).default('#6b7280'),
});

export const taskTags = pgTable('task_tags', {
  taskId: integer('task_id').references(() => tasks.id).notNull(),
  tagId: integer('tag_id').references(() => tags.id).notNull(),
}, (table) => ({
  pk: primaryKey({ columns: [table.taskId, table.tagId] }),
}));

// Relations — these define how Drizzle resolves
// nested queries in the relational query API
export const usersRelations = relations(users, ({ many }) => ({
  ownedProjects: many(projects),
  assignedTasks: many(tasks),
}));

export const projectsRelations = relations(projects, ({ one, many }) => ({
  owner: one(users, {
    fields: [projects.ownerId],
    references: [users.id],
  }),
  tasks: many(tasks),
}));

export const tasksRelations = relations(tasks, ({ one, many }) => ({
  project: one(projects, {
    fields: [tasks.projectId],
    references: [projects.id],
  }),
  assignee: one(users, {
    fields: [tasks.assigneeId],
    references: [users.id],
  }),
  taskTags: many(taskTags),
}));

Several things are worth noting about this schema definition. First, everything is standard TypeScript. There are no decorators, no magic strings, no custom file formats. Your editor’s autocomplete, refactoring tools, and Go to Definition feature all work exactly as they do with any other TypeScript code. Second, the schema includes indexing strategies, constraints, and enum definitions — the kind of database-level details that many ORMs either hide or make difficult to express. If you are serious about PostgreSQL performance tuning, the ability to define indexes declaratively alongside your tables is invaluable. Third, the relations definitions are separate from the table definitions. This is a deliberate design choice: relations in Drizzle are not database-level foreign key constraints (though those are defined via .references() on columns) but rather instructions to the relational query API about how to resolve nested queries.

Two Query APIs: Choose Your Level of Abstraction

One of Drizzle’s most distinctive features is that it provides two separate query APIs, each designed for different use cases. This dual-API approach reflects a pragmatic understanding that different queries have different requirements, and a single API cannot optimally serve all of them.

The SQL-like query builder is designed for developers who think in SQL. It provides a chainable API that maps almost one-to-one to SQL syntax: select(), from(), where(), join(), groupBy(), orderBy(), having(), and limit() all work exactly as you would expect. If you know SQL, you know the Drizzle query builder. There is virtually no new syntax to learn, and the generated SQL is predictable and inspectable. This is a significant advantage when you need to optimize query performance or debug unexpected results — you can look at the Drizzle query and immediately understand what SQL it will produce.

The relational query API is designed for queries that traverse relationships. It provides a Prisma-like syntax for fetching entities with their related data in a single call. Under the hood, it generates efficient SQL with joins or subqueries (depending on the relationship type), but the API lets you think in terms of entities and relationships rather than tables and joins.

Here is a practical example showing both APIs in action, handling the kind of queries you would encounter when building a real application:

// queries.ts — Practical Drizzle ORM query examples
import { eq, and, gte, lte, desc, sql, like, inArray, count } from 'drizzle-orm';
import { db } from './database';
import { users, projects, tasks, tags, taskTags } from './schema';

// ============================================
// SQL-LIKE QUERY BUILDER — full control
// ============================================

// Find all high-priority incomplete tasks for a user
// with their project names, ordered by due date
async function getUserUrgentTasks(userId: number) {
  return db
    .select({
      taskId: tasks.id,
      taskTitle: tasks.title,
      priority: tasks.priority,
      dueDate: tasks.dueDate,
      projectName: projects.name,
      projectStatus: projects.status,
    })
    .from(tasks)
    .innerJoin(projects, eq(tasks.projectId, projects.id))
    .where(
      and(
        eq(tasks.assigneeId, userId),
        eq(tasks.isCompleted, false),
        inArray(tasks.priority, ['high', 'critical']),
        // Type-safe — TypeScript ensures 'high' and 'critical'
        // are valid enum values at compile time
      )
    )
    .orderBy(desc(tasks.priority), tasks.dueDate)
    .limit(20);

  // Generated SQL is predictable and inspectable:
  // SELECT t.id, t.title, t.priority, t.due_date,
  //        p.name, p.status
  // FROM tasks t
  // INNER JOIN projects p ON t.project_id = p.id
  // WHERE t.assignee_id = $1
  //   AND t.is_completed = false
  //   AND t.priority IN ('high', 'critical')
  // ORDER BY t.priority DESC, t.due_date ASC
  // LIMIT 20
}

// Aggregate query — tasks per project with completion rates
async function getProjectStats(ownerId: number) {
  return db
    .select({
      projectId: projects.id,
      projectName: projects.name,
      totalTasks: count(tasks.id),
      completedTasks: count(
        sql`CASE WHEN ${tasks.isCompleted} = true THEN 1 END`
      ),
      completionRate: sql<number>`
        ROUND(
          COUNT(CASE WHEN ${tasks.isCompleted} = true THEN 1 END)::numeric
          / NULLIF(COUNT(${tasks.id}), 0) * 100, 1
        )
      `,
    })
    .from(projects)
    .leftJoin(tasks, eq(tasks.projectId, projects.id))
    .where(eq(projects.ownerId, ownerId))
    .groupBy(projects.id, projects.name)
    .orderBy(desc(sql`completion_rate`));
}

// Transaction with error handling
async function completeTaskAndNotify(taskId: number) {
  return db.transaction(async (tx) => {
    const [updatedTask] = await tx
      .update(tasks)
      .set({
        isCompleted: true,
      })
      .where(eq(tasks.id, taskId))
      .returning();

    if (!updatedTask) {
      tx.rollback();
      throw new Error(`Task ${taskId} not found`);
    }

    // Check if all tasks in the project are done
    const [remaining] = await tx
      .select({ count: count() })
      .from(tasks)
      .where(
        and(
          eq(tasks.projectId, updatedTask.projectId),
          eq(tasks.isCompleted, false)
        )
      );

    if (remaining.count === 0) {
      await tx
        .update(projects)
        .set({ status: 'completed' })
        .where(eq(projects.id, updatedTask.projectId));
    }

    return updatedTask;
  });
}

// ============================================
// RELATIONAL QUERY API — nested data fetching
// ============================================

// Fetch a project with its owner, tasks, and task assignees
// in a single call — similar to Prisma's include syntax
async function getProjectWithDetails(projectId: number) {
  return db.query.projects.findFirst({
    where: eq(projects.id, projectId),
    with: {
      owner: {
        columns: {
          id: true,
          displayName: true,
          avatarUrl: true,
        },
      },
      tasks: {
        orderBy: [desc(tasks.priority), tasks.createdAt],
        where: eq(tasks.isCompleted, false),
        with: {
          assignee: {
            columns: {
              id: true,
              displayName: true,
              avatarUrl: true,
            },
          },
          taskTags: {
            with: {
              // Would need a tag relation defined
              // on taskTags for this to work
            },
          },
        },
      },
    },
  });
  // Returns a fully typed object:
  // {
  //   id: number;
  //   name: string;
  //   status: 'planning' | 'active' | ...;
  //   owner: { id: number; displayName: string; avatarUrl: string | null };
  //   tasks: Array<{
  //     id: number;
  //     title: string;
  //     priority: 'low' | 'medium' | 'high' | 'critical';
  //     assignee: { id: number; displayName: string; ... } | null;
  //     ...
  //   }>;
  // }
}

// List users with their task counts
async function getUsersWithTaskCounts() {
  return db.query.users.findMany({
    columns: {
      id: true,
      displayName: true,
      email: true,
      role: true,
    },
    with: {
      assignedTasks: {
        columns: { id: true, isCompleted: true },
      },
    },
  });
}

Notice how the SQL-like query builder gives you explicit control over joins, aggregations, and raw SQL expressions while maintaining full type safety. The return type of getUserUrgentTasks is automatically inferred as an array of objects with exactly the fields specified in the select() call, each with the correct TypeScript type. If you try to access a field that does not exist in the result, TypeScript catches the error at compile time. If you pass a value of the wrong type to a where() clause, TypeScript catches that too. This level of type safety is comparable to what Prisma offers but without the code generation step.

Drizzle Kit: Migrations Done Right

Database migrations are one of the most error-prone parts of application development. A bad migration can corrupt data, cause downtime, or create inconsistencies that take hours to debug. Drizzle Kit, the CLI companion to Drizzle ORM, handles migrations with an approach that prioritizes transparency and developer control.

When you run drizzle-kit generate, Drizzle compares your current TypeScript schema definitions against a snapshot of the previous schema state and generates a standard SQL migration file containing the exact DDL statements needed to bring the database in sync. These migration files are plain SQL — not JSON, not a custom format, not an abstraction layer. You can read them, understand exactly what they will do, modify them if needed, and run them through your existing database migration pipeline.

This approach stands in contrast to Prisma’s migration system, which generates SQL but strongly discourages manual editing, and to TypeORM’s synchronize mode, which applies schema changes directly without generating migration files at all (a practice that is extremely dangerous in production). Drizzle’s philosophy is that migrations are too important to hide behind abstractions. The developer should always be able to see and understand exactly what will happen to the database.

Drizzle Kit also includes drizzle-kit push, which applies schema changes directly to the database without generating migration files. This is designed explicitly for rapid prototyping and development — not for production use. The distinction between generate (for production) and push (for development) reflects a mature understanding of the different requirements at different stages of the development lifecycle.

Performance Characteristics: Why Lightweight Matters

Drizzle’s performance story is one of its strongest selling points, and it stems directly from its architectural decisions. Because Drizzle does not run a separate query engine, does not perform runtime type validation, and does not maintain an internal identity map or cache, the overhead between your application code and the database driver is minimal. In benchmarks, Drizzle’s query execution time is within single-digit milliseconds of raw driver calls for most query patterns.

This matters more than you might think, especially in modern deployment environments. If you are building applications that run on serverless platforms like AWS Lambda, Cloudflare Workers, or Vercel Edge Functions, every millisecond of cold start time and every megabyte of bundle size directly affects your costs and user experience. Prisma’s Rust-based query engine adds significant cold start overhead and memory usage in serverless environments — a well-documented pain point that has driven many teams to evaluate alternatives. Drizzle, with its thin JavaScript runtime, has essentially no cold start penalty and a minimal memory footprint.

Drizzle also supports serverless database providers natively. It has first-class drivers for Neon (serverless PostgreSQL), PlanetScale (serverless MySQL), Turso (edge SQLite based on libSQL), Cloudflare D1, and Vercel Postgres. These integrations are not afterthoughts — they are core to Drizzle’s design philosophy. The team recognizes that the database landscape is shifting toward edge and serverless architectures, and they have built Drizzle to be an excellent citizen in these environments.

For teams already using Bun as their JavaScript runtime, Drizzle works seamlessly. Bun’s built-in SQLite driver can be used directly with Drizzle, providing a fully type-safe database layer with exceptional performance for applications that can use embedded SQLite.

Input Validation with Drizzle-Zod

One of Drizzle’s most practical ecosystem integrations is drizzle-zod, which automatically generates Zod validation schemas from your Drizzle table definitions. This eliminates the common problem of maintaining separate but semantically identical schema definitions for your database layer and your validation layer.

When you define a varchar('email', { length: 255 }).notNull() column in your Drizzle schema, drizzle-zod automatically generates a Zod schema that validates the field as a required string with a maximum length of 255 characters. Integer columns become z.number(), booleans become z.boolean(), enums become z.enum() with the exact values from your database enum, and nullable columns become z.nullable(). The generated schemas can be used for API request validation, form validation, or any other context where you need to validate data before it reaches the database.

This integration reflects Drizzle’s broader philosophy of composition over abstraction. Rather than building a proprietary validation system into the ORM, Drizzle provides clean interoperability with the validation tool that most TypeScript developers already use. Your Drizzle schema remains the single source of truth, and your Zod schemas are derived from it automatically.

How Drizzle Compares to Alternatives

The TypeScript ORM landscape has several mature options, and choosing between them requires understanding the specific tradeoffs each one makes.

Drizzle vs. Prisma: Prisma offers a more opinionated, batteries-included experience. Its GUI tools (Prisma Studio), its data modeling language, and its extensive documentation make it excellent for teams that want a complete, well-documented solution. Drizzle offers more control, better performance, and a smaller footprint, but expects you to be comfortable with SQL concepts and to assemble your own tooling for things like database GUIs and seed scripts. If you are building for serverless or edge environments, Drizzle’s architectural advantages are significant. If you are building a traditional Node.js application and value comprehensive tooling over raw performance, Prisma may be the better choice.

Drizzle vs. Kysely: Kysely is a type-safe SQL query builder — not a full ORM. It does not include schema definitions, migrations, or a relational query API. If you want a minimal query builder and are comfortable managing schema and migrations separately, Kysely is an excellent choice. Drizzle provides all of Kysely’s query-building capabilities plus schema management, migrations, and the relational query API, making it the more complete solution for most projects.

Drizzle vs. TypeORM: TypeORM is the most established TypeScript ORM, but its type safety is significantly weaker than Drizzle’s. TypeORM relies heavily on decorators and runtime reflection, which means many errors that Drizzle catches at compile time will only surface at runtime with TypeORM. TypeORM also has a larger surface area and more complex configuration requirements. For new TypeScript projects, Drizzle is the technically superior choice in almost every dimension.

When evaluating these tools for a professional project, it helps to consider how established agencies approach technology selection. Companies like Toimi evaluate framework and tooling choices based on long-term maintainability, team productivity, and deployment requirements — exactly the dimensions where Drizzle’s design decisions show their value.

Integration with Modern Frameworks

Drizzle’s lightweight architecture makes it an excellent fit for modern TypeScript frameworks. It integrates naturally with Hono, the ultra-fast web framework that shares Drizzle’s philosophy of minimal overhead. Both tools are designed for edge and serverless environments, and using them together gives you a full-stack solution with remarkably low latency.

For Next.js applications, Drizzle works with both the Pages Router and the App Router, including Server Components and Server Actions. Because Drizzle queries return plain objects (not model instances with methods), they can be passed directly from Server Components to Client Components without serialization issues — a subtle but important advantage over ORMs that return class instances.

The combination of Drizzle with a backend framework that supports connection pooling, such as those built on platforms like Supabase or Neon, provides a robust foundation for applications that need to handle significant traffic. Drizzle’s connection handling is delegated entirely to the underlying driver, which means you get the full benefit of whatever connection pooling and management strategy your database provider supports.

Real-World Considerations and Limitations

No tool is perfect, and Drizzle has genuine limitations that you should consider before adopting it.

The documentation, while improving rapidly, is less comprehensive than Prisma’s. Prisma has invested heavily in documentation, tutorials, and guides, and its documentation covers edge cases and advanced patterns that Drizzle’s docs sometimes do not address. The Drizzle community (primarily on Discord and GitHub) is active and helpful, but you will occasionally need to read source code to understand behavior that is not documented.

The relational query API, while powerful, has some limitations compared to Prisma’s. Deeply nested queries with complex filtering and ordering at multiple levels can sometimes produce unexpected SQL or hit edge cases that require falling back to the SQL-like query builder. The Drizzle team is actively improving this, but it is worth noting that the relational API is the younger and less battle-tested of the two query APIs.

Drizzle’s ecosystem is smaller than Prisma’s. There are fewer third-party integrations, fewer tutorials and courses, and fewer community-built tools. This is changing rapidly — Drizzle is one of the fastest-growing database tools in the TypeScript ecosystem — but if you need a specific integration that exists for Prisma but not for Drizzle, that gap may be a deciding factor.

Finally, Drizzle expects you to understand SQL. This is simultaneously a strength and a limitation. For experienced developers, the SQL-like query API is a feature — it means they can transfer their SQL knowledge directly and predict the generated queries confidently. For developers who are less comfortable with SQL, the learning curve may be steeper than with Prisma, which deliberately abstracts SQL away.

Getting Started: From Zero to First Query

Setting up Drizzle in a new project is straightforward. You install the core package (drizzle-orm), the CLI tool (drizzle-kit), and the driver for your database. For PostgreSQL with the popular postgres driver (also known as Postgres.js), the setup takes less than five minutes. You define your schema in a TypeScript file, configure drizzle.config.ts with your database connection string, run drizzle-kit push to apply the schema to your development database, and start writing queries. There is no generation step, no build step, no CLI wizard. You write TypeScript, and it works.

For teams migrating from another ORM, Drizzle provides drizzle-kit introspect, which connects to an existing database and generates Drizzle schema definitions from the current database structure. This means you can adopt Drizzle incrementally — introspect your existing schema, start writing new queries with Drizzle, and gradually migrate existing queries as time permits.

The combination of TypeScript-native schema definitions, predictable SQL generation, minimal runtime overhead, and excellent serverless support makes Drizzle ORM a compelling choice for modern TypeScript applications. It does not try to replace SQL or hide the database behind layers of abstraction. Instead, it gives you the full power of SQL with the full safety of TypeScript, and gets out of your way so you can focus on building your application.

Frequently Asked Questions

Is Drizzle ORM ready for production use?

Yes, Drizzle ORM is production-ready and is used by numerous companies in production environments handling significant traffic. The core query builder and the PostgreSQL, MySQL, and SQLite dialects are stable and well-tested. The migration system generates standard SQL files that can be reviewed and version-controlled. That said, as with any relatively young tool, you should evaluate it against your specific requirements, test thoroughly with your workload patterns, and keep the ORM updated to benefit from ongoing improvements and bug fixes.

How does Drizzle ORM performance compare to raw SQL queries?

Drizzle ORM adds minimal overhead compared to raw SQL queries executed directly through a database driver. Because Drizzle does not maintain an identity map, does not perform runtime type validation, and does not run a separate query engine process, the latency between a Drizzle query and the equivalent raw SQL call is typically in the low single-digit milliseconds range. For serverless and edge deployments, Drizzle’s cold start time is also negligible compared to ORMs that bundle a separate query engine, making it one of the most performant ORM options available for TypeScript.

Can I use Drizzle ORM with an existing database that already has data?

Yes, Drizzle supports working with existing databases through the drizzle-kit introspect command. This connects to your database, reads its current schema, and generates the corresponding Drizzle TypeScript schema files. You can then use these generated schemas as your starting point and begin writing type-safe queries immediately. This makes it possible to adopt Drizzle incrementally in an existing project without rewriting your entire data layer at once, and without requiring any changes to your existing database structure.

Does Drizzle ORM support database transactions?

Yes, Drizzle ORM has full support for database transactions across all supported dialects (PostgreSQL, MySQL, and SQLite). Transactions use a callback-based API where you receive a transaction object that provides the same query interface as the main database object. You can perform multiple queries within a transaction, and they will either all commit or all roll back. Drizzle also supports nested transactions (savepoints) on databases that support them, and provides a rollback() method for explicit transaction cancellation within the callback.

What databases and serverless platforms does Drizzle ORM support?

Drizzle ORM supports three database dialects: PostgreSQL, MySQL, and SQLite. Within each dialect, it supports multiple drivers and platforms. For PostgreSQL, it works with node-postgres, Postgres.js, Neon Serverless, Vercel Postgres, and Supabase. For MySQL, it supports mysql2 and PlanetScale Serverless. For SQLite, it supports better-sqlite3, Bun’s built-in SQLite, Cloudflare D1, and Turso (libSQL). This broad driver support makes Drizzle a versatile choice regardless of whether you are deploying to traditional servers, serverless functions, or edge computing platforms.