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-orm
pnpm add better-drizzle drizzle-orm
yarn add better-drizzle drizzle-orm
bun add better-drizzle drizzle-orm

better-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:

schema.ts
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(...):

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

Where to next

On this page