better-drizzle

better-drizzle vs Drizzle

What the wrapper adds on top of raw Drizzle, what it costs, and when raw Drizzle is still the better tool.

better-drizzle is not a replacement for Drizzle. It wraps the Drizzle client you already have and adds a repository API per table. Your schema, driver, and raw queries stay the same, and both clients share one connection.

Short answer

Use better-drizzle when most of your database code is application CRUD: lookups, lists with relations, pagination, relation writes, and the same cross-cutting rules (timestamps, soft delete, tenant isolation) on every query.

Keep raw Drizzle for queries that are really SQL: reports with GROUP BY, window functions, CTEs, INSERT ... SELECT, or a hot path you have profiled. You can do both in the same codebase, and in the same transaction.

What better-drizzle adds

NeedRaw Drizzlebetter-drizzle
Point lookupsselect ... limit 1, then rows[0] ?? nullfindUnique, findFirst, plus .throw() when the row is required
Nested relationsdb.query or joins and manual stitchinginclude / select at any depth, with per-level where, orderBy, take, skip
Relation filtersexists subqueries by handsome, every, none, is, isNot
Relation countscorrelated subqueriesinclude: { _count: { select: { posts: true } } }
Paginationtwo queries and the page mathpaginate() and cursor() return { data, pagination }
Relation writesinserts and deletes on the junction tableconnect, disconnect, set, in an implicit transaction
Bulk writesbuilders per dialectcreateMany, upsertMany, updateEach (one UPDATE ... CASE)
Atomic updatessql expressions{ increment: 1 }, { toggle: true }, array and JSONB path updates on PostgreSQL
Transactionsdb.transactionsavepoints for nested calls, retries, afterCommit / afterRollback
Cross-cutting rulesrepeated in every queryhooks and plugins: timestamps, soft delete, tenant, cache, Zod, UUID, guardrails
Errorsdriver-specific error shapesBetterDrizzleError with a stable code, and helpers like isUniqueViolation

Side by side

const post = await client.posts.findUnique({
	where: { id },
	include: { author: { select: { name: true } } },
});
const authors = await client.users.findMany({
	where: { active: true, posts: { some: { published: true } } },
});

Count and exists

const total = await client.posts.count({ where: { published: true } });
const taken = await client.users.exists({ where: { email } });

A full example with pagination, per-parent limits, and relation counts is on Why better-drizzle.

What it costs

The numbers come from the API-parity benchmarks: raw Drizzle does the same work and returns the same shape, and both sides run in the same sampling window on SQLite in-memory.

AreaOverhead vs raw Drizzle
Readswithin ~12%, most within 9%
Prepared readswithin ~4% of Drizzle's own prepared statements
Writeswithin ~5%
Transactionswithin ~9%
Relation graph (users → posts → comments)9.5× faster than db.query (414.30 µs vs 3.94 ms)

Memory is mixed: single reads use less heap, while mixed-read batches and transactions use more. See benchmark parity for how the pairs are kept fair.

When raw Drizzle is the better tool

  • Reports and analytics: groupBy, sum, avg, window functions, CTEs. Delegates only have count, exists, and _count.
  • Set-based statements: INSERT ... SELECT, UPDATE ... FROM.
  • SQL the API does not model: expressions in ORDER BY, full-text ranking, extension functions.
  • A profiled hot path where a few microseconds matter.

See limitations for the full list.

You can always drop down

The client you pass to better() keeps working, and the better-drizzle client has raw SQL on it:

import { count } from 'drizzle-orm';

const client = better(db);

// raw Drizzle, same connection
const stats = await db
	.select({ authorId: posts.authorId, total: count() })
	.from(posts)
	.groupBy(posts.authorId);

// raw SQL inside a better-drizzle transaction
await client.transaction(async (tx) => {
	await tx.posts.update({ where: { id }, data: { published: true } });
	await tx.$executeRaw`update stats set published = published + 1`;
});

where also accepts a Drizzle sql fragment, and client.users.$where() turns a better-drizzle filter into a Drizzle condition for your own queries.

Next steps

On this page