better-drizzle vs raw Drizzle

What better-drizzle changes, what it leaves alone, and when to reach for raw Drizzle instead.

better-drizzle is a thin repository layer over Drizzle. The goal is to remove repeated query glue while keeping Drizzle's type-safety and staying close to the metal. This page makes the trade-off concrete.

The same query, both ways

A common request: published posts whose author is active, newest first, with the author attached, projected for an API response.

Raw Drizzle
import { and, desc, eq } from 'drizzle-orm';

const rows = await db
	.select({
		id: posts.id,
		title: posts.title,
		author: { id: users.id, name: users.name },
	})
	.from(posts)
	.innerJoin(users, eq(posts.authorId, users.id))
	.where(and(eq(posts.published, true), eq(users.active, true)))
	.orderBy(desc(posts.id))
	.limit(20);
better-drizzle
const rows = await client.posts.findMany({
	where: {
		published: true,
		author: { is: { active: true } },
	},
	select: {
		id: true,
		title: true,
		author: { select: { id: true, name: true } },
	},
	orderBy: [{ id: 'desc' }],
	take: 20,
});

Both are fully typed. The difference shows up when you have dozens of these across a codebase — the second form is the same shape every time, and the relation join is derived from your schema instead of hand-written.

What it changes

AreaRaw Drizzlebetter-drizzle
Point lookupsdb.select().from().where(eq(...)) + unwrapfindUnique({ where: { email } })
Relation loadingmanual joins or db.query configinclude / select with inferred payloads
Nested relation filtershand-built exists subqueriessome / every / none / is
Paginationrebuild count + hasNext each timepaginate(){ data, pagination }
Not-found handlingcheck for undefined everywherenullable result or .throw()
Cross-cutting concernssprinkle through call sitesclient hooks and plugins
Timestamps / soft deleterepeat in every writeofficial plugins

What it leaves alone

  • Your schema. Tables and relations stay in Drizzle, exactly as you write them today.
  • Your driver. SQLite, PostgreSQL, MySQL — you pick it; the dialect is detected.
  • Raw SQL. First-class $raw / $executeRaw for anything the delegate API should not model.
  • Drizzle expressions. You can pass a Drizzle sql fragment straight into where.
  • Performance posture. The wrapper is measured against raw Drizzle and uses fast paths for common operations.

When to use raw Drizzle instead

better-drizzle is additive, so "both" is usually the answer. Reach for raw Drizzle (directly, or via $raw) when:

  • you need database-specific SQL functions or window functions
  • a reporting query reads better as SQL than as a builder
  • you are writing a one-off statement that the delegate API should not try to model
  • you are in a measured hot path and want to hand-tune the exact query

It is not all-or-nothing

The Better client and your raw Drizzle db operate on the same connection and the same transaction. Use the repository API for routine CRUD and drop to raw SQL where it earns its place — even inside the same transaction.

How it compares to a full ORM

If you have used Prisma, the delegate API will feel familiar — findMany, where, include, select, upsert. The difference is that better-drizzle generates that surface from your Drizzle schema and compiles down to Drizzle queries, so there is no separate schema language, no generate step, and no client process. You keep Drizzle's model and add the ergonomics.

On this page