Nathaniel's blog
Back to posts

Drizzle ORM: Type-Safe Database Queries in TypeScript

Nathaniel LinFebruary 27, 20268 min read1 views
Drizzle ORM: Type-Safe Database Queries in TypeScript

Drizzle ORM is a TypeScript-first database toolkit that generates SQL you can actually read. Unlike Prisma (which has its own query engine) or TypeORM (which uses decorators), Drizzle maps directly to SQL constructs with full type inference.

Schema Definition

Drizzle schemas are plain TypeScript — no decorators, no separate schema files:

import { pgTable, uuid, text, timestamp, integer } from "drizzle-orm/pg-core";
import { pgEnum } from "drizzle-orm/pg-core";

export const visibilityEnum = pgEnum("visibility", [
  "draft",
  "published",
  "private",
]);

export const posts = pgTable("posts", {
  id: uuid("id").defaultRandom().primaryKey(),
  title: text("title").notNull(),
  slug: text("slug").notNull().unique(),
  content: text("content").notNull().default(""),
  visibility: visibilityEnum("visibility").notNull().default("draft"),
  viewCount: integer("view_count").notNull().default(0),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

The table definition is the single source of truth. TypeScript infers insert and select types automatically:

type Post = typeof posts.$inferSelect;
type NewPost = typeof posts.$inferInsert;

Query Builders

Drizzle has two APIs: the SQL-like query builder and the relational query API.

SQL-like API

Feels like writing SQL in TypeScript:

const recentPosts = await db
  .select({
    id: posts.id,
    title: posts.title,
    viewCount: posts.viewCount,
  })
  .from(posts)
  .where(eq(posts.visibility, "published"))
  .orderBy(desc(posts.createdAt))
  .limit(10);

The return type is automatically { id: string; title: string; viewCount: number }[] — no manual typing needed.

Relational API

For complex queries with joins, the relational API is cleaner:

const postsWithTags = await db.query.posts.findMany({
  where: eq(posts.visibility, "published"),
  with: {
    tags: {
      with: {
        tag: true,
      },
    },
    author: {
      columns: {
        name: true,
        email: true,
      },
    },
  },
  orderBy: [desc(posts.createdAt)],
  limit: 10,
});

This generates efficient JOINs and nests the results automatically. The with clause follows your relation definitions.

Relations

Define relations separately from tables for clean separation:

import { relations } from "drizzle-orm";

export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.ownerId],
    references: [users.id],
  }),
  tags: many(postsTags),
}));

export const postsTagsRelations = relations(postsTags, ({ one }) => ({
  post: one(posts, {
    fields: [postsTags.postId],
    references: [posts.id],
  }),
  tag: one(tags, {
    fields: [postsTags.tagId],
    references: [tags.id],
  }),
}));

Migrations

Drizzle Kit handles schema migrations:


# Generate migration from schema diff
pnpm drizzle-kit generate

# Apply migrations
pnpm drizzle-kit migrate

# Or push schema directly (dev only)
pnpm drizzle-kit push

The push command is great for rapid development — it compares your schema to the database and applies changes directly. For production, use generate + migrate for versioned, reviewable migrations.

Why Drizzle?

  • Zero runtime overhead: Queries compile to plain SQL strings

  • Full TypeScript inference: No code generation step (unlike Prisma)

  • SQL knowledge transfers: If you know SQL, you know Drizzle

  • Serverless-friendly: No query engine binary, no connection management issues

  • Multiple databases: PostgreSQL, MySQL, SQLite with the same API

The trade-off is less magic. Drizzle won't auto-generate a CRUD API or manage migrations automatically. But what you get is a transparent, type-safe SQL layer that respects your existing SQL knowledge.

Share this post

Reactions