Type-safe repository helpers for Drizzle.
Keep Drizzle’s type-safety. Drop the repetitive query glue. better-drizzle wraps your client and gives every table reads, writes, relation loading, pagination, hooks, and plugins — without giving up the metal.
$ npm install better-drizzle drizzle-ormimport { better } from 'better-drizzle';
const client = better(db, { schema });
const posts = await client.posts.findMany({
where: {
published: true,
author: { is: { active: true } },
},
include: { author: true },
orderBy: [{ id: 'desc' }],
take: 3,
});The same query, without the glue
Both are fully typed. The difference is the dozens of these you write across a codebase — and which one you’d rather read.
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);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,
});Everything you rewrite, once
A consistent repository API per table — the patterns every service ends up re-implementing, generated from your schema and kept typed.
Typed nested filters
Query across relations with some / every / none / is — inferred from your Drizzle schema, no subqueries by hand.
One pagination shape
Offset and cursor both return { data, pagination } with count, hasNext, and hasPrevious. Stop rebuilding it.
Transactions & savepoints
Nested transactions, opt-in retries, explicit rollback, and afterCommit / afterRollback callbacks — all on one client.
First-class plugins
Timestamps, soft delete, and your own — with transforms, lifecycle hooks, and typed operation args.
Lifecycle hooks
Audit, trace, and authorize in one place instead of threading it through every call site.
Raw SQL, when you want it
$raw, $executeRaw, and guarded $rawUnsafe are first-class. Drop to SQL only when it genuinely reads better.
Close to the metal
Measured against raw Drizzle with fair, API-parity comparisons. Reads are often faster through the wrapper — and use less memory.
Numbers from the repository’s suite (SQLite in-memory). See the full benchmarks →
Works with your existing database
better-drizzle stays on top of Drizzle, so your driver choice does not change.
PostgreSQL
Typed delegates on top of the Drizzle pg stack.
SQLite
Fast local dev and benchmark-friendly in-memory setups.
MySQL
Same API surface on top of mysql-backed Drizzle clients.
Plugins do the cross-cutting work
Timestamps and soft delete ship as official plugins. They add typed arguments, rewrite operations, and extend delegates — so behavior lives in one place instead of every write.
import { better } from 'better-drizzle';
import { timestamps } from '@better-drizzle/timestamps';
import { softDelete } from '@better-drizzle/soft-delete';
const client = better(db, {
schema,
plugins: [
timestamps(),
softDelete({
column: 'deletedAt',
defaults: { visibility: 'without' },
}),
],
});
await client.users.delete({
where: { id: 1 },
mode: 'soft',
}); // typed plugin arg
await client.users.findMany({ deleted: 'only' }); // typed filter
await client.users.restore({ where: { id: 1 } }); // plugin method