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.4pnpm add better-drizzle drizzle-orm@1.0.0-rc.4yarn add better-drizzle drizzle-orm@1.0.0-rc.4bun add better-drizzle drizzle-orm@1.0.0-rc.4better-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:
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),
});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),
});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),
});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),
});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),
});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:
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.
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.
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);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.
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.
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);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