better-drizzleDrizzle ORM, but better

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-orm
posts.ts
import { 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.

Raw Drizzle
raw-drizzle.ts
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
better-drizzle.ts
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.

−85%heap on reads vs raw Drizzle
0–18%read latency overhead at parity
< 4%write overhead
0codegen or build steps

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.

db.ts
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