home / skills / martinffx / claude-code-atelier / atelier-typescript-drizzle-orm

This skill helps you design and query SQL with Drizzle ORM in TypeScript, enabling type-safe schemas, relations, migrations, and multi-database support.

npx playbooks add skill martinffx/claude-code-atelier --skill atelier-typescript-drizzle-orm

Review the files below or copy the command above to add this skill to your agents.

Files (6)
SKILL.md
9.6 KB
---
name: atelier-typescript-drizzle-orm
description: Type-safe SQL with Drizzle ORM in TypeScript. Use when defining database schemas, writing queries, setting up relations, running migrations, or working with PostgreSQL/MySQL/SQLite/Cloudflare D1/Durable Objects data layers.
user-invocable: false
---

# 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)

```typescript
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](./references/postgresql.md) for detailed PostgreSQL patterns.

## Quick Start (SQLite/D1)

```typescript
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](./references/sqlite.md) for SQLite patterns and [references/cloudflare.md](./references/cloudflare.md) for D1 and Durable Objects.

## Type Inference

```typescript
// 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

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

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

```typescript
// 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

```typescript
// Single insert
const [newUser] = await db
  .insert(users)
  .values({ name: 'Alice', email: '[email protected]' })
  .returning()

// Multiple insert
await db.insert(users).values([
  { name: 'Bob', email: '[email protected]' },
  { name: 'Carol', email: '[email protected]' },
])

// Upsert (on conflict)
await db
  .insert(users)
  .values({ name: 'Alice', email: '[email protected]' })
  .onConflictDoUpdate({
    target: users.email,
    set: { name: 'Alice Updated' },
  })
```

## Update

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

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

```typescript
await db.transaction(async (tx) => {
  const [user] = await tx
    .insert(users)
    .values({ name: 'Alice', email: '[email protected]' })
    .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.

```typescript
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](./references/entity-pattern.md) for detailed examples.

## Repository Pattern

Repositories wrap database access with error handling and business logic.

```typescript
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](./references/repository-pattern.md) for detailed examples.

## Database-Specific Guides

For database-specific patterns, connection setup, migrations, and testing:

- **[PostgreSQL patterns](./references/postgresql.md)** - Connection, migrations, column types, error codes, optimistic locking
- **[SQLite patterns](./references/sqlite.md)** - Schema definition, type differences, better-sqlite3 testing
- **[Cloudflare D1 & Durable Objects](./references/cloudflare.md)** - D1 connection, DO SQLite, testing with vitest-pool-workers, D1 vs DO decision guide

## Guidelines

1. Define schema in dedicated `schema.ts` file(s)
2. Use `$inferSelect` and `$inferInsert` for types - don't duplicate
3. Always define relations for nested queries with `db.query`
4. Pass `{ schema }` to `drizzle()` to enable relational queries
5. Use SQL-like API (`db.select()`) for complex joins
6. Use relational API (`db.query`) for nested data fetching
7. Foreign keys need explicit `references(() => table.column)`
8. Use `returning()` to get inserted/updated/deleted rows
9. Wrap database access in Repository classes for error handling
10. Use Entity classes for all data transformations (fromRequest, fromRecord, toRecord, toResponse)
11. Add `lockVersion` column for optimistic locking on mutable resources
12. Handle DB errors with specific error types (23505=conflict, 23503=not found, 40001/OC000=retry)

Overview

This skill provides a TypeScript-first atelier for building type-safe SQL models and queries with Drizzle ORM. It guides schema definitions, relational queries, migrations, and patterns for PostgreSQL, MySQL, SQLite, Cloudflare D1 and Durable Objects. The focus is on spec-driven development, clear entity and repository patterns, and predictable, typed database access.

How this skill works

You define tables with db-core helpers (pgTable, sqliteTable, column types) and infer TypeScript types from schema ($inferSelect, $inferInsert). Relations are declared with relations(...) and passed into drizzle() so you can use a relational query API (db.query.*) for nested loading and db.select()/SQL-like API for joins. The skill demonstrates inserts, updates, deletes, transactions, upserts, and migration-aware patterns across supported adapters.

When to use it

  • Designing and maintaining database schemas in TypeScript
  • Implementing type-safe queries and joins for relational data
  • Building repositories and entities that encapsulate DB logic
  • Running migrations or adapting to PostgreSQL/MySQL/SQLite/D1/DO backends
  • Writing tests that assert DB behavior with typed records

Best practices

  • Keep schema declarations in dedicated schema files and infer types—avoid duplicating interfaces
  • Define relations explicitly and pass schema to drizzle() to enable nested queries
  • Wrap DB access in Repository classes to centralize error handling and business rules
  • Use Entity classes to transform between API, domain, and DB shapes (fromRequest/fromRecord/toRecord/toResponse)
  • Use returning() for insert/update/delete to get canonical DB records and avoid manual mapping
  • Add lockVersion or similar optimistic locking for mutable resources and handle DB-specific error codes

Example use cases

  • Create a users/posts schema with foreign keys and cascade deletes, then fetch posts with author data via relational queries
  • Implement an upsert flow for unique-email users with onConflictDoUpdate and returning() to obtain the saved record
  • Use transactions to atomically create a user and their initial posts, rolling back on failure
  • Write repository methods that map DB errors to domain errors (conflict, not found, retryable)
  • Port an app from SQLite to PostgreSQL by reusing typed schema and adapting minor column type differences

FAQ

How do I get TypeScript types for rows and inserts?

Use the schema-inferred helpers: export type User = typeof users.$inferSelect and export type NewUser = typeof users.$inferInsert.

When should I use db.select() vs db.query?

Use db.select() and joins for complex SQL-like queries and performance-tuned queries; use db.query.* (relational API) for convenient nested loading and partial column selection when schema relations are defined.

How do I handle unique constraint conflicts?

Use insert(...).onConflictDoUpdate({ target: users.email, set: { ... } }) and catch DB errors, mapping constraint codes (e.g., 23505) to domain-level conflict errors.