drizzle / orm / typescript
Why we reach for Drizzle, and the one query pattern that pays off
Schema as plain TypeScript, two query styles, and the single-statement relational reads that sold us. Plus the honest 2026 caveat now that Prisma 7 went pure TypeScript.
- Published
- May 30, 2026
- Read
- 3 min
Contents
Every project reaches the moment where you pick an ORM, and the choice quietly shapes the next two years of the codebase. We have shipped on Prisma, on raw SQL, and for most of our recent work on Drizzle. This is not a takedown of the alternatives. It is an honest account of why Drizzle fits the way we like to work, and the one query pattern that earns its keep.
Schema as plain TypeScript
Drizzle's schema is just TypeScript. You declare tables with sqliteTable or pgTable, and the row types are inferred, not generated. There is no codegen step that can drift out of sync with your code.
export const posts = sqliteTable('posts', {
id: text('id').primaryKey(),
slug: text('slug').notNull().unique(),
title: text('title').notNull(),
status: text('status', { enum: ['draft', 'published'] }).notNull(),
})
type Post = typeof posts.$inferSelect
type NewPost = typeof posts.$inferInsertThat $inferSelect gives you the exact row type with zero build steps. When you rename a column, the type errors show up immediately in every file that touches it. For a small team, that tight feedback loop is most of the value.
Two query styles, and when to use each
Drizzle gives you two ways to read data, and mixing them up is the most common point of confusion. The SQL-like builder reads almost exactly like the query it produces:
const rows = await db
.select()
.from(posts)
.where(eq(posts.status, 'published'))
.orderBy(desc(posts.publishedAt))It returns flat rows, and it never pretends to be anything other than SQL. When you need nested relations, you switch to the relational query builder, db.query, which returns shaped objects:
const post = await db.query.posts.findFirst({
where: eq(posts.slug, slug),
with: { author: true, comments: { limit: 10 } },
})The reason this matters: the relational query builder always emits exactly one SQL statement, even for deeply nested relations. There is no N+1, no fan-out of round trips, just one query to the database. On a join-heavy page that is the difference between snappy and sluggish, and it is the single feature that sold us.
One catch worth flagging, because it is the most common setup error: db.query only exists if you pass your full schema into the client, and your relations have to be declared with the relations helper. Forget either and db.query is simply undefined.
Migrations that stay readable
The tooling is drizzle-kit. You run drizzle-kit generate to diff your schema and emit a plain SQL migration file, then drizzle-kit migrate to apply it. The migrations are SQL you can read and review, not an opaque format. For prototyping there is drizzle-kit push to sync the schema directly, and drizzle-kit studio gives you a GUI when you want to poke at data.
The honest 2026 caveat
For a long time the pitch for Drizzle was thin runtime, small bundle, fast cold starts on serverless and edge. That was a real edge over Prisma's old query engine. In 2026 the picture shifted: Prisma 7 dropped its Rust engine and went pure TypeScript, which narrows that gap considerably. So if your only reason for choosing Drizzle was cold-start time, that argument is weaker now.
The durable reasons remain. Drizzle keeps you close to SQL, so you are never guessing what query ran. The single-statement relational reads are genuinely fast. And the no-codegen, inferred types fit a workflow where the schema changes often and you want the compiler shouting at you the moment it does. That combination, not bundle size, is why it stays our default.