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 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; // string

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

9.1×faster relation loading at parity
< 9%read latency overhead at parity
< 5%write overhead at parity
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.

OUR SPONSORSponsor
Neon

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.

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

Our Sponsors

Thanks to companies backing better-drizzle and the work around it.