better-drizzle

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.

Using a coding agent? Copy the setup prompt and paste it into Claude Code, Codex, Cursor, or Copilot:

Drizzle ORM 1.x only

better-drizzle supports only Drizzle ORM 1.x (drizzle-orm >=1.0.0-rc.4 <1.0.0-rc.5; later release candidates are not supported yet) with its defineRelations(...) relations API. drizzle-orm 0.x is not supported by the current release - projects on 0.x must stay on better-drizzle 0.2.x. See upgrading.

Install

better-drizzle sits on top of Drizzle, so install both. Pick your package manager:

npm install better-drizzle drizzle-orm@1.0.0-rc.4
pnpm add better-drizzle drizzle-orm@1.0.0-rc.4
yarn add better-drizzle drizzle-orm@1.0.0-rc.4
bun add better-drizzle drizzle-orm@1.0.0-rc.4

better-drizzle declares drizzle-orm and typescript as peer dependencies. Use Drizzle >=1.0.0-rc.4 <1.0.0-rc.5 and TypeScript ^5. Install drizzle-orm with the explicit version above: until Drizzle 1.0 is tagged latest on npm, a plain drizzle-orm install resolves to 0.x.

Define a schema

The schema file holds tables only. A small users → posts schema:

schema.ts
import { boolean, integer, pgTable, text } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
	id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
	email: text('email').notNull().unique(),
	name: text('name').notNull(),
	active: boolean('active').notNull().default(true),
});

export const posts = pgTable('posts', {
	id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
	authorId: integer('author_id')
		.notNull()
		.references(() => users.id),
	title: text('title').notNull(),
	published: boolean('published').notNull().default(false),
});
schema.ts
import { boolean, int, mysqlTable, text, varchar } from 'drizzle-orm/mysql-core';

export const users = mysqlTable('users', {
	id: int('id').primaryKey().autoincrement(),
	email: varchar('email', { length: 255 }).notNull().unique(),
	name: text('name').notNull(),
	active: boolean('active').notNull().default(true),
});

export const posts = mysqlTable('posts', {
	id: int('id').primaryKey().autoincrement(),
	authorId: int('author_id')
		.notNull()
		.references(() => users.id),
	title: text('title').notNull(),
	published: boolean('published').notNull().default(false),
});
schema.ts
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),
});
schema.ts
import { boolean, integer, pgTable, text } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
	id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
	email: text('email').notNull().unique(),
	name: text('name').notNull(),
	active: boolean('active').notNull().default(true),
});

export const posts = pgTable('posts', {
	id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
	authorId: integer('author_id')
		.notNull()
		.references(() => users.id),
	title: text('title').notNull(),
	published: boolean('published').notNull().default(false),
});
schema.ts
import { boolean, int, mysqlTable, text, varchar } from 'drizzle-orm/mysql-core';

export const users = mysqlTable('users', {
	id: int('id').primaryKey().autoincrement(),
	email: varchar('email', { length: 255 }).notNull().unique(),
	name: text('name').notNull(),
	active: boolean('active').notNull().default(true),
});

export const posts = mysqlTable('posts', {
	id: int('id').primaryKey().autoincrement(),
	authorId: int('author_id')
		.notNull()
		.references(() => users.id),
	title: text('title').notNull(),
	published: boolean('published').notNull().default(false),
});
schema.ts
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),
});

Define relations

Relations are declared with Drizzle's defineRelations. better-drizzle reads them - together with the tables - to generate each table's typed API:

relations.ts
import { defineRelations } from 'drizzle-orm';
import * as schema from './schema';

export const relations = defineRelations(schema, (r) => ({
	users: {
		posts: r.many.posts(),
	},
	posts: {
		author: r.one.users({
			from: r.posts.authorId,
			to: r.users.id,
		}),
	},
}));

Relations power the nice parts

The include, select, and nested relation filters all come from the relations you define here. Declaring users.posts / posts.author is what makes where: { author: { is: { ... } } } possible and typed. With no relations, export defineRelations(schema) without a callback.

Create the client

Create a normal Drizzle client with relations, then wrap it once with better(...):

Install the pg driver too.

db.ts
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/node-postgres';
import { relations } from './relations';

const db = drizzle({ connection: process.env.DATABASE_URL!, relations });

export const client = better(db);

Install the mysql2 driver too.

db.ts
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/mysql2';
import { relations } from './relations';

const db = drizzle({ connection: { uri: process.env.DATABASE_URL! }, relations });

export const client = better(db);
db.ts
import Database from 'bun:sqlite';
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { relations } from './relations';

const sqlite = new Database('app.db');
const db = drizzle({ client: sqlite, relations });

export const client = better(db);

Install the pg driver too.

db.ts
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/node-postgres';
import { relations } from './relations';

const db = drizzle({ connection: process.env.DATABASE_URL!, relations });

export const client = better(db);

Install the mysql2 driver too.

db.ts
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/mysql2';
import { relations } from './relations';

const db = drizzle({ connection: { uri: process.env.DATABASE_URL! }, relations });

export const client = better(db);
db.ts
import Database from 'bun:sqlite';
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { relations } from './relations';

const sqlite = new Database('app.db');
const db = drizzle({ client: sqlite, relations });

export const client = better(db);

better() reads tables and relations from the Drizzle instance, so relations must be passed to drizzle(). Without it, better() throws No tables found on the Drizzle instance. Options such as plugins and hooks go in the optional second argument: better(db, { plugins: [...] }).

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