# Getting started

> Install better-drizzle, define a Drizzle schema, create the client, and run your first typed queries.

URL: https://better-drizzle.com/docs/getting-started

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](/docs/guides/upgrading-0-3).

## Install

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

**npm**

```bash
npm install better-drizzle drizzle-orm@1.0.0-rc.4
```

**pnpm**

```bash
pnpm add better-drizzle drizzle-orm@1.0.0-rc.4
```

**yarn**

```bash
yarn add better-drizzle drizzle-orm@1.0.0-rc.4
```

**bun**

```bash
bun add better-drizzle drizzle-orm@1.0.0-rc.4
```

> **Note:** 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:

**PostgreSQL**

```ts title="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),
});
```

**MySQL**

```ts title="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),
});
```

**SQLite**

```ts title="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:

```ts title="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(...)`:

**PostgreSQL**

Install the `pg` driver too.

```ts title="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);
```

**MySQL**

Install the `mysql2` driver too.

```ts title="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);
```

**SQLite**

```ts title="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

```ts
// 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](/docs/guides/dynamic-repositories))
* 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:

```ts
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

- [Reads & filters](/docs/querying/reads): The read API and the filter model.

- [Relations](/docs/querying/relations): `include`, `select`, and nested relation filters.

- [CRUD & mutations](/docs/writing/crud): Create, update, delete, and upsert.

- [Transactions](/docs/advanced/transactions): Savepoints, retries, and lifecycle callbacks.
