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 authors = await client.users.findMany({
where: {
active: true,
posts: { some: { published: true } },
},
include: {
_count: { select: { posts: { where: { published: true } } } },
posts: {
where: { published: true },
orderBy: [{ score: 'desc' }],
take: 3,
},
},
take: 20,
});
authors[0]._count.posts; // number
authors[0].posts[0].title; // stringThe 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. Typed JSONB path filters on PostgreSQL.
Batched relation loading
Nested include and select run one query per relation node - no N+1, no cartesian blowup. Project relation totals with _count without an extra round-trip.
Relational writes
connect, disconnect, and exclusive set on create and update. Junction tables are inferred, and the whole write runs in one implicit transaction.
One pagination shape
Use paginate() for offset pages and cursor() for feed-style navigation. Both return { data, pagination } without rebuilding metadata by hand.
Query plans, inline
Every read helper is a thenable with .explain(). Get a structured, cross-dialect plan - including deferred relation stages - without running the query twice.
Row locks
lock, skipLocked, and noWait on PostgreSQL and MySQL, with an opt-in guard that rejects locked reads outside a transaction.
First-class plugins
Rules, Zod, timestamps, and soft delete ship in the box - with transforms, lifecycle hooks, and typed operation args you can add yourself.
Guardrails, static and runtime
better-drizzle/eslint catches what a linter can see; better-drizzle/rules enforces the rest at runtime - raw SQL, destructive writes, unbounded reads.
Raw SQL, when you want it
$raw, $executeRaw, and guarded $rawUnsafe are first-class, with their own hooks. Drop to SQL only when it genuinely reads better.
Close to the metal
Measured against raw Drizzle with fair, API-parity comparisons. Relation loading is fasterthrough the wrapper; the rest stays close - and where it doesn’t, the benchmarks say so.
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.
Neon
Serverless Postgres for modern Drizzle workflows.
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
Rules, Zod, timestamps, and soft delete ship as official plugins - all inside the one better-drizzle package. They add typed arguments, rewrite operations, and extend delegates, so behavior lives in one place instead of every write.
// one package - every plugin is a subpath export
import { better } from 'better-drizzle';
import { recommended, rules } from 'better-drizzle/rules';
import { softDelete } from 'better-drizzle/soft-delete';
import { timestamps } from 'better-drizzle/timestamps';
import { zod } from 'better-drizzle/zod';
const client = better(db, {
schema,
plugins: [
rules(recommended({ noRawUnsafe: true })),
zod({ validate: { create: true, update: true } }),
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
client.users.$zod.create; // generated Zod schema