typescript:drizzle-orm
Drizzle ORM
Lightweight, type-safe ORM with SQL-like and relational query APIs for PostgreSQL, MySQL, SQLite, Cloudflare D1, and Durable Objects.
Quick Start (PostgreSQL)
import {
pgTable,
serial,
text,
integer,
timestamp,
boolean,
varchar,
uuid,
primaryKey,
unique,
index
} from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
age: integer('age'),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').$onUpdate(() => new Date()),
})
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
See references/postgresql.md for detailed PostgreSQL patterns.
Quick Start (SQLite/D1)
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
isActive: integer('is_active', { mode: 'boolean' }).default(true),
createdAt: text('created_at').notNull(),
})
export const posts = sqliteTable('posts', {
id: text('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: text('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: text('created_at').notNull(),
})
See references/sqlite.md for SQLite patterns and references/cloudflare.md for D1 and Durable Objects.
Type Inference
// Infer types from schema - no manual interfaces needed
export type User = typeof users.$inferSelect
export type NewUser = typeof users.$inferInsert
export type Post = typeof posts.$inferSelect
export type NewPost = typeof posts.$inferInsert
Relations
import { relations } from 'drizzle-orm'
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}))
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}))
SQL-like Queries
import { eq, and, or, gt, like, isNull, desc, asc } from 'drizzle-orm'
// Select all
const allUsers = await db.select().from(users)
// Select specific columns
const names = await db.select({ name: users.name }).from(users)
// Where clause
const activeUsers = await db
.select()
.from(users)
.where(eq(users.isActive, true))
// Multiple conditions
const filtered = await db
.select()
.from(users)
.where(and(
eq(users.isActive, true),
gt(users.age, 18)
))
// Like/pattern matching
const matching = await db
.select()
.from(users)
.where(like(users.email, '%@example.com'))
// Order and limit
const recent = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(10)
// Joins
const postsWithAuthors = await db
.select({
postTitle: posts.title,
authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
Relational Queries
// Requires schema with relations passed to drizzle()
const db = drizzle(pool, { schema })
// Find many with relations
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
})
// Partial columns + nested relations
const partial = await db.query.users.findMany({
columns: {
id: true,
name: true,
},
with: {
posts: {
columns: {
title: true,
createdAt: true,
},
},
},
})
// Find first
const user = await db.query.users.findFirst({
where: eq(users.id, 1),
with: { posts: true },
})
// Exclude columns
const withoutEmail = await db.query.users.findMany({
columns: {
email: false, // exclude
},
})
Insert
// Single insert
const [newUser] = await db
.insert(users)
.values({ name: 'Alice', email: 'alice@example.com' })
.returning()
// Multiple insert
await db.insert(users).values([
{ name: 'Bob', email: 'bob@example.com' },
{ name: 'Carol', email: 'carol@example.com' },
])
// Upsert (on conflict)
await db
.insert(users)
.values({ name: 'Alice', email: 'alice@example.com' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'Alice Updated' },
})
Update
await db
.update(users)
.set({ isActive: false })
.where(eq(users.id, 1))
// Update with returning
const [updated] = await db
.update(users)
.set({ name: 'New Name' })
.where(eq(users.id, 1))
.returning()
Delete
await db.delete(users).where(eq(users.id, 1))
// Delete with returning
const [deleted] = await db
.delete(users)
.where(eq(users.id, 1))
.returning()
Transactions
await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ name: 'Alice', email: 'alice@example.com' })
.returning()
await tx.insert(posts).values({
title: 'First Post',
authorId: user.id,
})
})
Entity Pattern
Domain entities encapsulate data transformations between API, domain, and database layers.
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import type { users } from './schema'
type UserRecord = InferSelectModel<typeof users>
type UserInsert = InferInsertModel<typeof users>
class UserEntity {
public readonly id: string
public readonly name: string
public readonly email: string
public readonly createdAt: Date
private constructor(data: UserEntityData) {
Object.assign(this, data)
}
// API request → Entity
static fromRequest(rq: CreateUserRequest, id?: string): UserEntity {
return new UserEntity({
id: id ?? crypto.randomUUID(),
name: rq.name,
email: rq.email,
createdAt: new Date(),
})
}
// DB record → Entity
static fromRecord(record: UserRecord): UserEntity {
return new UserEntity({
id: record.id,
name: record.name,
email: record.email,
createdAt: record.createdAt,
})
}
// Entity → DB insert
toRecord(): UserInsert {
return {
id: this.id,
name: this.name,
email: this.email,
createdAt: this.createdAt,
}
}
// Entity → API response
toResponse(): UserResponse {
return {
id: this.id,
name: this.name,
email: this.email,
createdAt: this.createdAt.toISOString(),
}
}
}
See references/entity-pattern.md for detailed examples.
Repository Pattern
Repositories wrap database access with error handling and business logic.
import { eq, and } from 'drizzle-orm'
import { users } from './schema'
import { UserEntity } from './entities/UserEntity'
class UserRepo {
constructor(private db: DrizzleDB) {}
async getById(id: string): Promise<UserEntity> {
const record = await this.db.query.users.findFirst({
where: eq(users.id, id),
})
if (!record) throw new NotFoundError('User not found')
return UserEntity.fromRecord(record)
}
async create(entity: UserEntity): Promise<UserEntity> {
try {
const [record] = await this.db
.insert(users)
.values(entity.toRecord())
.returning()
return UserEntity.fromRecord(record)
} catch (error) {
throw handleDBError(error, { userId: entity.id })
}
}
async update(entity: UserEntity): Promise<UserEntity> {
const [record] = await this.db
.update(users)
.set(entity.toRecord())
.where(eq(users.id, entity.id))
.returning()
if (!record) throw new NotFoundError('User not found')
return UserEntity.fromRecord(record)
}
}
See references/repository-pattern.md for detailed examples.
Database-Specific Guides
For database-specific patterns, connection setup, migrations, and testing:
- PostgreSQL patterns - Connection, migrations, column types, error codes, optimistic locking
- SQLite patterns - Schema definition, type differences, better-sqlite3 testing
- Cloudflare D1 & Durable Objects - D1 connection, DO SQLite, testing with vitest-pool-workers, D1 vs DO decision guide
Guidelines
- Define schema in dedicated
schema.tsfile(s) - Use
$inferSelectand$inferInsertfor types - don't duplicate - Always define relations for nested queries with
db.query - Pass
{ schema }todrizzle()to enable relational queries - Use SQL-like API (
db.select()) for complex joins - Use relational API (
db.query) for nested data fetching - Foreign keys need explicit
references(() => table.column) - Use
returning()to get inserted/updated/deleted rows - Wrap database access in Repository classes for error handling
- Use Entity classes for all data transformations (fromRequest, fromRecord, toRecord, toResponse)
- Add
lockVersioncolumn for optimistic locking on mutable resources - Handle DB errors with specific error types (23505=conflict, 23503=not found, 40001/OC000=retry)
More from martinffx/claude-code-atelier
python:architecture
Python application architecture with functional core, effectful shell, DDD, and data modeling. Use when designing application layers, separating pure business logic from IO, defining domain models, implementing validation, or structuring bounded contexts.
14python:monorepo
Python monorepo architecture with uv workspaces, mise, and apps/packages pattern. Use when setting up project structure, configuring workspaces, managing dependencies across packages, or designing multi-app Python repositories.
13python:build-tools
Python project tooling with uv, mise, ruff, basedpyright, and pytest. Use when setting up pyproject.toml, running builds, typechecking, configuring tests, linting, formatting, or managing Python environments.
12python:modern-python
Modern Python language features and typing patterns. Use when writing type hints, using generics, implementing pattern matching, working with async/await, or leveraging Python 3.10+ features.
12python:testing
Stub-Driven TDD and layer boundary testing with pytest. Use when writing tests, deciding what to test, testing at component boundaries, or implementing test-driven development.
12python:sqlalchemy
SQLAlchemy ORM patterns for Python database access. Use when defining models, writing queries, implementing upserts, working with JSON columns, or managing database sessions.
12