Database Patterns (Drizzle ORM)

VerifiedSafe

Provides database patterns for PostgreSQL using Drizzle ORM, including a repository-based data access layer. Enforces conventions like snake_case naming, UUID primary keys, and timestamps. Includes migration workflow and common query patterns for filtering.

Sby Skills Guide Bot
DevelopmentIntermediate
1706/2/2026
Claude Code
#database#drizzle-orm#postgresql#repository-pattern#migrations

Recommended for

Our review

Provides PostgreSQL database patterns using Drizzle ORM, including schemas, repositories, and migration workflows.

Strengths

  • Query encapsulation through reusable repository classes
  • Clear conventions for table and column naming
  • Integrated migration workflow with standardized naming

Limitations

  • Limited to PostgreSQL and Drizzle ORM
  • Does not cover testing or data seeding
  • Generic repositories can become verbose for simple cases
When to use it

When building an application with Drizzle ORM and you want a consistent, maintainable data access layer structure.

When not to use it

If the database is very simple and can be handled directly, or if you're using a different ORM.

Security analysis

Safe
Quality score92/100

The skill provides code patterns and best practices for Drizzle ORM usage. It contains no destructive, exfiltrating, or evasive instructions. All commands shown are standard development operations (e.g., migrations) and no sensitive data handling is demonstrated.

No concerns found

Examples

Create Repository
Create a new repository for the 'comments' table following Drizzle ORM patterns with findById, create, update, delete methods.
Generate Migration
Generate a migration named 'add_category_to_posts' to add a text column 'category' to the posts table using Drizzle ORM.

Database Skill (Drizzle ORM)

This skill provides database patterns for PostgreSQL with Drizzle ORM. Run /db for full reference.

Project Structure

src/db/
├── index.ts           # Singleton db connection
├── schema.ts          # All table definitions
├── migrate.ts         # Migration runner
└── repositories/      # Data access layer (REQUIRED)

CRITICAL: Always Use Repositories

NEVER access db directly. Always use repository classes.

// ✅ CORRECT
import { featureRepository } from '@/db/repositories/feature.repository';
const feature = await featureRepository.findById(id);

// ❌ WRONG - Direct db access
import { db } from '@/db';
const feature = await db.select().from(features); // NEVER DO THIS

Schema Definition

// src/db/schema.ts
import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  id: uuid('id').defaultRandom().primaryKey(),
  title: text('title').notNull(),
  content: text('content'),
  userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
  status: text('status').notNull().default('draft'),
  priority: integer('priority').notNull().default(0),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

// Type exports
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

Schema Conventions

  • Table names: lowercase plural (users, posts)
  • Column names: snake_case (created_at, user_id)
  • Primary keys: UUID with defaultRandom()
  • Timestamps: Always include created_at and updated_at

Repository Pattern

// src/db/repositories/post.repository.ts
import { eq, and, desc, asc } from 'drizzle-orm';
import { db, schema } from '@/db';
import type { Post, NewPost } from '@/db/schema';

export class PostRepository {
  async findById(id: string): Promise<Post | null> {
    return await db.query.posts.findFirst({
      where: eq(schema.posts.id, id),
    }) ?? null;
  }

  async findMany(limit = 10, offset = 0): Promise<Post[]> {
    return await db.query.posts.findMany({
      limit,
      offset,
      orderBy: desc(schema.posts.createdAt),
    });
  }

  async findByUserId(userId: string): Promise<Post[]> {
    return await db.query.posts.findMany({
      where: eq(schema.posts.userId, userId),
      orderBy: (posts, { desc }) => [desc(posts.createdAt)],
    });
  }

  async create(data: NewPost): Promise<Post> {
    const result = await db
      .insert(schema.posts)
      .values({
        ...data,
        createdAt: new Date(),
        updatedAt: new Date(),
      })
      .returning();
    return result[0];
  }

  async update(id: string, data: Partial<NewPost>): Promise<Post | null> {
    const result = await db
      .update(schema.posts)
      .set({
        ...data,
        updatedAt: new Date(),
      })
      .where(eq(schema.posts.id, id))
      .returning();
    return result[0] ?? null;
  }

  async delete(id: string): Promise<boolean> {
    const result = await db
      .delete(schema.posts)
      .where(eq(schema.posts.id, id))
      .returning();
    return result.length > 0;
  }
}

export const postRepository = new PostRepository();

Migration Workflow

# 1. Edit src/db/schema.ts
# 2. Generate migration
bun db:generate --name=create_posts_table

# 3. Review SQL in drizzle/migrations/
# 4. Apply migration
bun db:migrate

# 5. Commit migration files
git add drizzle/migrations/

Migration Naming

  • Use snake_case: create_posts_table, add_user_role_column
  • Start with action: create_, add_, update_, remove_

Common Query Patterns

Filtering with Multiple Conditions

import { eq, and, or, isNull, isNotNull, gt, lt, like } from 'drizzle-orm';

// AND conditions
where: and(
  eq(schema.posts.userId, userId),
  eq(schema.posts.status, 'published')
)

// OR conditions
where: or(
  eq(schema.posts.status, 'draft'),
  eq(schema.posts.status, 'review')
)

// NULL checks
where: isNull(schema.posts.deletedAt)

// Comparisons
where: gt(schema.posts.priority, 5)

// LIKE
where: like(schema.posts.title, '%search%')

Ordering

// Single column
orderBy: desc(schema.posts.createdAt)

// Multiple columns
orderBy: (posts, { asc, desc }) => [
  asc(posts.status),
  desc(posts.priority),
  desc(posts.createdAt),
]

Relations (with query API)

// In schema.ts - define relations
import { relations } from 'drizzle-orm';

export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.userId],
    references: [users.id],
  }),
  comments: many(comments),
}));

// In repository - query with relations
async findWithAuthor(id: string) {
  return await db.query.posts.findFirst({
    where: eq(schema.posts.id, id),
    with: {
      author: true,
      comments: true,
    },
  });
}

Database Client Setup

// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

const client = postgres(process.env.DATABASE_URL!, {
  max: process.env.NODE_ENV === 'production' ? 10 : 1,
  idle_timeout: 20,
  max_lifetime: 60 * 30,
});

export const db = drizzle(client, { schema });
export type Database = typeof db;
export { schema };

Common Imports

// Schema and types
import { db, schema } from '@/db';
import type { Post, NewPost } from '@/db/schema';

// Query operators
import { eq, and, or, gt, lt, gte, lte, like, isNull, isNotNull, desc, asc } from 'drizzle-orm';

// Table definitions (for insert/update)
import { pgTable, uuid, text, timestamp, integer, jsonb, boolean } from 'drizzle-orm/pg-core';

// Relations
import { relations } from 'drizzle-orm';
Related skills