Back to blog
Tutorials

Drizzle ORM: TypeScript-First SQL Without the Magic

Kim BoenderKim Boender
April 5, 2026 7 min read
Drizzle ORM: TypeScript-First SQL Without the Magic

The ORM space has felt stagnant for a while. TypeORM was the default choice for TypeScript backends for years, but it's aged poorly, decorator-heavy, maintenance concerns, and some genuinely confusing behavior around relations. Prisma swept in and won a lot of hearts with its DX, but it operates at a layer of abstraction that makes you feel like you're not writing SQL at all, which is great until you need to do something slightly unusual and suddenly you're fighting the query engine.

Drizzle ORM takes a different approach. It's TypeScript-first, stays close to SQL, weighs in at ~7.4kb, and has been one of the most talked-about libraries in the JS backend ecosystem over the past year. Here's my take after using it on NestJS projects.

What Makes Drizzle Different

Most ORMs abstract SQL into their own query language. Drizzle does the opposite, it embraces SQL and just makes it type-safe. If you know SQL, you already know most of Drizzle's API. Queries look like queries, not method chains that could mean anything.

The other standout properties:

  • Zero dependencies, 7.4kb minified and gzipped, tree-shakeable
  • Serverless-first, stateless by design, works great on edge runtimes
  • Two query modes, a SQL-like builder and a higher-level relational API
  • Drizzle Kit, a companion CLI for schema migrations
  • Supports PostgreSQL, MySQL, SQLite, Turso, Neon, PlanetScale, and more

Installation

For a NestJS project with PostgreSQL:

npm install drizzle-orm pg
npm install --save-dev drizzle-kit @types/pg

Create a database connection:

// src/db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
})

export const db = drizzle(pool, { schema })

Defining Your Schema

This is where Drizzle's TypeScript-first design really shows. Schema definitions are plain TypeScript objects, no decorators, no class magic:

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

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: text('name').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

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

The schema is your source of truth, Drizzle infers TypeScript types from it automatically. No separate type declarations, no @Column() decorators scattered through class bodies.

Basic Queries

Selects read exactly like SQL:

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

// Select all users
const allUsers = await db.select().from(users)

// Select with conditions
const activeUser = await db
  .select({
    id: users.id,
    email: users.email,
    name: users.name,
  })
  .from(users)
  .where(eq(users.email, 'kim@example.com'))
  .limit(1)

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

// Update
await db
  .update(users)
  .set({ name: 'Updated Name' })
  .where(eq(users.id, 1))

// Delete
await db.delete(posts).where(eq(posts.authorId, 1))

Notice there's nothing surprising here. The API mirrors SQL closely enough that you can usually reason through what query Drizzle will produce without checking the docs.

Relations and the Relational Query API

Drizzle has two APIs for working with related data. The first is joins, written explicitly like you would in raw SQL. The second is the relational API, which feels closer to Prisma's include.

Define relations in a separate file:

// src/db/relations.ts
import { defineRelations } from 'drizzle-orm'
import { users, posts } from './schema'

export const relations = defineRelations({ users, posts }, (r) => ({
  users: {
    posts: r.many(posts, {
      from: users.id,
      to: posts.authorId,
    }),
  },
  posts: {
    author: r.one(users, {
      from: posts.authorId,
      to: users.id,
    }),
  },
}))

Then query with the relational API:

// Fetch all users with their posts
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
})

// Fetch a post with its author
const post = await db.query.posts.findFirst({
  where: eq(posts.id, 42),
  with: {
    author: {
      columns: { email: true, name: true },
    },
  },
})

The relational API is cleaner for typical CRUD and association fetching. The SQL builder is better when you need aggregates, complex joins, or anything that doesn't fit a standard findMany/findOne pattern.

Having both available is genuinely useful rather than feeling like an afterthought.

Migrations with Drizzle Kit

Drizzle Kit handles schema migrations as a companion CLI. Add a config file:

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

export default {
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
} satisfies Config

Then generate migrations from your schema changes:

# Generate migration SQL from schema diff
npx drizzle-kit generate

# Apply migrations
npx drizzle-kit migrate

# Open Drizzle Studio, a browser UI to explore your data
npx drizzle-kit studio

The migrations are plain SQL files that get generated into the drizzle/ folder. You can review and commit them alongside your code. No magic runtime migration discovery, just files you control.

Drizzle Studio is worth calling out separately, it's a clean browser-based database explorer that runs locally via the CLI. Not a must-have, but nice to have during development.

NestJS Integration

Drizzle doesn't have an official NestJS module, but it's straightforward to wire up as a provider:

// src/db/db.module.ts
import { Module, Global } from '@nestjs/common'
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'

export const DB = Symbol('DB')

@Global()
@Module({
  providers: [
    {
      provide: DB,
      useFactory: () => {
        const pool = new Pool({
          connectionString: process.env.DATABASE_URL,
        })
        return drizzle(pool, { schema })
      },
    },
  ],
  exports: [DB],
})
export class DbModule {}

Inject it in any service:

import { Inject, Injectable } from '@nestjs/common'
import { DB } from '../db/db.module'
import { NodePgDatabase } from 'drizzle-orm/node-postgres'
import * as schema from '../db/schema'
import { eq } from 'drizzle-orm'

@Injectable()
export class UsersService {
  constructor(
    @Inject(DB) private readonly db: NodePgDatabase<typeof schema>,
  ) {}

  async findById(id: number) {
    return this.db.query.users.findFirst({
      where: eq(schema.users.id, id),
    })
  }
}

The lack of an official NestJS module is a minor friction point, there are community packages like nestjs-drizzle that wrap this up. In practice I've found the manual wiring above clean enough that I don't bother with extra packages.

How It Compares

If you're coming from Prisma, the adjustment is primarily around schema definition (TypeScript vs .prisma files) and the fact that Drizzle doesn't generate a client from a schema at build time. Some people love Prisma's generated client; I find Drizzle's approach more predictable.

If you're coming from TypeORM, Drizzle is a substantial improvement in almost every dimension, no decorator hell, no confusing lazy/eager loading defaults, and significantly better performance.

The one real gap compared to Prisma is ecosystem maturity. Prisma has more guides, more third-party integrations, and a larger community. Drizzle is catching up fast, but if you're on a team that relies heavily on community resources, it's worth factoring in.

Is It Worth Adopting?

For new NestJS + PostgreSQL projects, I'd reach for Drizzle first today. The type safety is excellent, the bundle size is genuinely impressive for serverless and edge workloads, and writing SQL-like queries means the code is self-explanatory without needing to trace through abstraction layers.

For serverless deployments, on Neon, PlanetScale, or similar serverless Postgres platforms, Drizzle's stateless connection model is a particular advantage over Prisma's persistent connection assumptions.

If you're on an existing Prisma project that's working fine, there's no urgent reason to migrate. But if Prisma's abstraction is causing you friction on complex queries, Drizzle is worth evaluating seriously. The migration path is less painful than it might look.

Frequently Asked Questions

What's the main difference between Drizzle ORM and Prisma? +
Prisma uses a separate .prisma schema file and generates a type-safe client at build time, abstracting SQL behind its own query language. Drizzle defines schemas directly in TypeScript, stays close to SQL syntax, and has zero runtime dependencies — making it smaller and more predictable, especially for complex queries or serverless environments.
Does Drizzle ORM support MongoDB? +
Not currently, Drizzle is designed for SQL databases and supports PostgreSQL, MySQL, SQLite, Turso, Neon, and PlanetScale. If you need MongoDB with TypeScript, Mongoose with strict types or the official MongoDB driver with Zod validation are the more common choices. Drizzle has signaled interest in NoSQL support on its roadmap, but there's no timeline yet.
How do I run database migrations with Drizzle? +
Drizzle uses its companion CLI, Drizzle Kit. You define a drizzle.config.ts pointing at your schema file, then run `npx drizzle-kit generate` to produce plain SQL migration files and `npx drizzle-kit migrate` to apply them. Migrations are stored as readable SQL in your project, so you can review and commit them alongside your code.
Why does Drizzle have two query APIs — the SQL builder and the relational API? +
They serve different use cases. The SQL builder (select/insert/update/delete) maps directly to SQL and is best for aggregates, complex joins, and anything non-standard. The relational API (db.query.table.findMany) is a higher-level abstraction closer to Prisma's include syntax, cleaner for straightforward CRUD and association fetching. You can mix both in the same project.
Is Drizzle ORM production-ready for large applications in 2026? +
Yes, Drizzle has been production-used by a growing number of teams and has been benchmarked as the fastest ORM for Node.js workloads in 2025/2026. The main consideration for large teams is ecosystem maturity: Prisma has more community guides and third-party integrations. Drizzle is catching up quickly, and for greenfield projects its type safety and performance make it a strong default.

Try it yourself

JSON Formatter

Format, validate, and beautify JSON instantly

Open JSON Formatter