https://github.com/almeidazs/better-drizzle
ORM, but better
https://github.com/almeidazs/better-drizzle
better database drizzle drizzle-orm mysql nodejs orm postgresql sqlite typescript
Last synced: about 3 hours ago
JSON representation
ORM, but better
- Host: GitHub
- URL: https://github.com/almeidazs/better-drizzle
- Owner: almeidazs
- License: apache-2.0
- Created: 2026-06-22T16:12:45.000Z (4 days ago)
- Default Branch: main
- Last Pushed: 2026-06-22T23:53:08.000Z (3 days ago)
- Last Synced: 2026-06-23T01:05:44.958Z (3 days ago)
- Topics: better, database, drizzle, drizzle-orm, mysql, nodejs, orm, postgresql, sqlite, typescript
- Language: TypeScript
- Homepage:
- Size: 184 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/funding.yml
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
Drizzle ORM, but better 🚀
Minimal, type-safe repository helpers for [Drizzle ORM](https://orm.drizzle.team).
[better-drizzle](https://npmjs.com/package/better-drizzle) wraps an existing Drizzle client and gives each table a small, consistent API for reads, writes, pagination, nested filters, relation loading, and optional hooks. The goal is simple: keep Drizzle's type-safety, remove repetitive query glue, and stay close enough to the metal that performance still matters.
```bash
npm install better-drizzle drizzle-orm
```
## Why
Drizzle is excellent when you want explicit SQL-first control.
It gets repetitive when every service ends up re-writing the same patterns:
- point lookups
- relation includes
- pagination payloads
- existence checks
- count helpers
- CRUD return shapes
- nested `where` filters
[better-drizzle](https://npmjs.com/package/better-drizzle) packages those patterns into a small repository-style API without trying to replace Drizzle itself.
## What it improves
- Less repeated query code for common CRUD flows
- Nested relation filters with Drizzle-backed typing
- `include` and `select` support with typed payload inference
- Unified pagination return shape
- Optional lifecycle hooks for cross-cutting behavior
- First-class plugins with setup, transforms, and client/model extensions
- Fast paths for simple reads and writes to reduce wrapper overhead
- Consistent table delegates: `findMany`, `findFirst`, `create`, `update`, `delete`, `paginate`, `count`, `exists`, `upsert`
Querying your database with Better client
```ts
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/better-sqlite3';
const db = drizzle(sqlite, { schema });
const client = better(db, { schema });
const user = await client.users.findFirst({
where: { id: 1 },
});
const posts = await client.posts.findMany({
where: {
published: true,
author: {
is: {
active: true,
},
},
},
include: {
author: true,
},
orderBy: [{ id: 'desc' }],
take: 20,
});
```
**Check whether a user exists or not and count after it.**
```ts
const exists = await client.users.exists({
where: { id: 123 },
});
const count = await client.users.count({
where: {
name: { contains: 'drizzle-orm' },
},
});
```
**Create and update the user.**
```ts
const someUser = await client.users.create({
data: {
id: 123,
name: 'better',
},
});
const user = await client.users.update({
data: {
name: 'better-drizzle',
},
where: { id: someUser.id },
});
const maybeCreated = await client.users.create({
data: {
email: 'better@example.com',
id: 124,
name: 'better-again',
},
skipDuplicates: true,
});
if (!maybeCreated) {
console.log('user already existed');
}
```
**You can where queries like drizzle too**
```ts
const { count } = await client.users.delete({
where: eq(users.id, 123),
});
```
**You can also resolve repositories dynamically.**
```ts
const users = client.repository('users');
```
The repository name can be the TypeScript schema key or the database table name.
## Transactions
Transactions live on the client, not on individual models. The callback receives a full Better Drizzle client bound to the underlying transaction, so model delegates, plugin args, transforms, hooks, and nested transactions all keep working.
```ts
const user = await client.transaction(async (tx) => {
const created = await tx.users.create({
data: {
email: 'better@example.com',
id: 123,
name: 'better',
},
});
tx.afterCommit(async () => {
await sendWelcomeEmail(created.email);
});
return created;
});
```
## Plugins
Plugins let you package setup logic, query transforms, and reusable client/model extensions without wrapping `better(...)` yourself.
Plugins can also extend the built-in operation args in a fully typed way through `operationArgs`, so custom fields like `deleted` or `mode` flow from the delegate call into plugin transforms and hooks.
```ts
import { better } from 'better-drizzle';
import { timestamps } from '@better-drizzle/timestamps';
import { softDelete } from '@better-drizzle/soft-delete';
const client = better(drizzle, {
schema,
plugins: [
timestamps({
createdAt: 'created_at',
updatedAt: 'updated_at',
}),
softDelete({
column: 'deletedAt',
deletedByColumn: 'deletedById',
defaults: {
mode: 'soft',
visibility: 'without',
},
}),
],
});
await client.users.delete({
where: { id: 1 },
});
await client.users.findMany({
deleted: 'only',
});
await client.users.restore({
where: { id: 1 },
});
```
Now you can soft delete rows easily and also have timestamps fields injected automatically.
## Hooks
The client accepts optional hooks through `better(db, options)`. This is useful for auditing, tracing, metrics, authorization, and other cross-cutting concerns that you do not want duplicated in every repository call.
The hook layer is optional. If you do not need it, do not pass it.
**Always assign a random UUID in the user before creating it.**
```ts
const client = better(drizzle, {
schema,
hooks: {
beforeCreate({ data: user }) {
user.organizationId = randomUUID();
},
},
});
```
## Performance
See the full benchmark suite and results in [`benchmark/README.md`](/benchmark). The suite covers reads, writes, and transactions (including nested savepoints) with fair API-parity comparisons against raw Drizzle.