Getting started
Install better-drizzle, define a Drizzle schema, create the client, and run your first typed queries.
This page shows the smallest realistic setup and the mental model behind it.
Install
better-drizzle sits on top of Drizzle, so install both. Pick your package manager:
npm install better-drizzle drizzle-ormpnpm add better-drizzle drizzle-ormyarn add better-drizzle drizzle-ormbun add better-drizzle drizzle-ormbetter-drizzle declares drizzle-orm and typescript as peer dependencies.
Use Drizzle ^0.45 and TypeScript ^5.
Define a schema
better-drizzle reads your Drizzle schema — including relations — to generate each table's typed API. A small users → posts schema:
import { relations } from 'drizzle-orm';
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
active: integer('active', { mode: 'boolean' }).notNull().default(true),
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey(),
authorId: integer('author_id')
.notNull()
.references(() => users.id),
title: text('title').notNull(),
published: integer('published', { mode: 'boolean' })
.notNull()
.default(false),
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
export const schema = {
users,
usersRelations,
posts,
postsRelations,
};Relations power the nice parts
The include, select, and nested relation filters all come from the
relations you define here. Defining usersRelations / postsRelations is
what makes where: { author: { is: { ... } } } possible and typed.
Create the client
Create a normal Drizzle client first, then wrap it once with better(...):
import Database from 'bun:sqlite';
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { schema } from './schema';
const sqlite = new Database('app.db');
const db = drizzle(sqlite, { schema });
export const client = better(db, { schema });The dialect (SQLite, PostgreSQL, MySQL) is detected from your Drizzle instance — the same client API works across all three.
Run your first queries
// A relation-aware list
const users = await client.users.findMany({
where: {
active: true,
},
include: {
posts: {
where: { published: true },
select: { id: true, title: true },
orderBy: [{ id: 'desc' }],
take: 3,
},
},
orderBy: [{ id: 'desc' }],
take: 20,
});
// A single row by unique field
const alice = await client.users.findUnique({
where: { email: 'alice@example.com' },
});
// Insert once, skip duplicates if the email already exists
const maybeCreated = await client.users.create({
data: {
email: 'alice@example.com',
name: 'Alice',
active: true,
},
skipDuplicates: ['email'],
});
// Bulk sync users with a native batch upsert
await client.users.upsertMany({
data: [
{ email: 'alice@example.com', name: 'Alice', active: true },
{ email: 'bob@example.com', name: 'Bob', active: false },
],
target: ['email'],
update: ['name', 'active'],
});
// A count and an existence check
const activeCount = await client.users.count({ where: { active: true } });
const exists = await client.users.exists({ where: { id: 1 } });What better(...) gives you
- one delegate per table:
client.users,client.posts - a dynamic lookup:
client.repository(name)(details) - typed reads:
findMany,findFirst,findOne,findUnique,count,exists - typed writes:
create,createMany,update,updateMany,delete,deleteMany,upsert,upsertMany - pagination, raw SQL, hooks, transactions, and plugins on the same client
A first transaction
Transactions use the same delegates and can register lifecycle callbacks:
await client.transaction(async (tx) => {
const user = await tx.users.create({
data: {
email: 'new@example.com',
name: 'New User',
active: true,
},
});
tx.afterCommit(() => {
console.log('Committed user', user.email);
});
return user;
});What stays explicit
better-drizzle does not hide your schema or replace Drizzle. You still:
- define tables and relations in Drizzle
- choose the database driver yourself
- decide when to drop to raw SQL
- decide how much abstraction your service layer wants