Claude Skills Guide

Claude Code for Drizzle ORM TypeScript Database Workflow

Drizzle ORM combined with TypeScript provides a powerful, type-safe approach to database operations. When you add Claude Code to this equation, you gain an intelligent partner that understands your schema, generates optimized queries, and helps you build robust data layers faster. This guide walks you through practical workflows for integrating Drizzle with TypeScript in your projects.

Why Drizzle ORM with TypeScript

Drizzle ORM distinguishes itself by embracing SQL directly while providing TypeScript type inference that feels magical. Unlike traditional ORMs that hide database complexity, Drizzle keeps your queries readable and your types exact. When you pair this with Claude Code’s understanding of TypeScript patterns, you get automated assistance that writes database code matching your exact schema.

The key advantage is compile-time safety. TypeScript catches relationship errors before runtime, and Drizzle’s query builder maintains full type information through every operation. Claude Code amplifies these benefits by generating correct queries based on your schema descriptions.

Setting Up Your TypeScript Project with Drizzle

Initialize your project with the necessary dependencies. Here’s a typical setup for a PostgreSQL project:

npm init -y
npm install drizzle-orm postgres
npm install -D drizzle-kit typescript @types/node

Create your TypeScript configuration:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  }
}

Configure Drizzle Kit with your database connection:

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL || 'postgres://localhost:5432/mydb',
  },
});

Claude Code can help you set up this configuration by describing your database requirements. Simply tell Claude your database type and connection details, and it generates the appropriate configuration.

Defining Type-Safe Schemas

Your schema definitions form the foundation of type-safe database operations. Drizzle’s declarative approach makes schemas easy to read and modify:

import { pgTable, serial, varchar, timestamp, boolean, integer, text } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }),
  role: varchar('role', { length: 50 }).default('user').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  userId: integer('user_id').references(() => users.id).notNull(),
  title: varchar('title', { length: 255 }).notNull(),
  content: text('content'),
  published: boolean('published').default(false).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

The $inferSelect and $inferInsert types give you perfect type safety for reading and writing operations. Claude Code uses these types to ensure every query returns and accepts the correct data shapes.

Database Connection Patterns

Managing database connections properly prevents common issues like connection exhaustion. Here’s a robust pattern for your database client:

import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString, { max: 10 });

export const db = drizzle(client);

For serverless environments, consider connection pooling with services like PgBouncer or use Drizzle’s edge adapters:

import { drizzle } from 'drizzle-orm/edge-runtime';
import { Pool } from 'pg';

// For edge functions
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool);

Claude Code can recommend the best connection strategy based on your deployment target. Describe your hosting environment, and Claude suggests the appropriate approach.

Building CRUD Operations

With your schema defined, CRUD operations become straightforward and fully typed. Here are essential patterns:

Creating Records

import { users } from './schema';
import { eq } from 'drizzle-orm';

const createUser = async (data: NewUser) => {
  const [user] = await db.insert(users).values(data).returning();
  return user;
};

// Usage with full type inference
const newUser = await createUser({
  email: 'developer@example.com',
  name: 'Alex Developer',
  role: 'engineer',
});

Reading Data

// Get single user by email
const getUserByEmail = async (email: string) => {
  const [user] = await db
    .select()
    .from(users)
    .where(eq(users.email, email))
    .limit(1);
  return user;
};

// Get all published posts with author info
const getPublishedPosts = async () => {
  return db
    .select({
      id: posts.id,
      title: posts.title,
      authorName: users.name,
      authorEmail: users.email,
    })
    .from(posts)
    .innerJoin(users, eq(posts.userId, users.id))
    .where(eq(posts.published, true))
    .orderBy(posts.createdAt);
};

Updating Records

const updateUser = async (id: number, data: Partial<NewUser>) => {
  const [updated] = await db
    .update(users)
    .set({ ...data, updatedAt: new Date() })
    .where(eq(users.id, id))
    .returning();
  return updated;
};

Deleting Records

const deletePost = async (id: number) => {
  await db.delete(posts).where(eq(posts.id, id));
};

Claude Code generates these operations based on natural language descriptions. Tell Claude what data you need to access or modify, and it constructs the correct query with proper types.

Working with Relationships

Drizzle’s relation system enables efficient data loading with full type safety. Define relationships in your schema:

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.userId],
    references: [users.id],
  }),
}));

Now you can use eager loading to fetch related data efficiently:

const getUserWithPosts = async (userId: number) => {
  return db.query.users.findFirst({
    where: eq(users.id, userId),
    with: {
      posts: {
        where: eq(posts.published, true),
        orderBy: posts.createdAt,
      },
    },
  });
};

The query builder maintains full type inference through the relationship chain. You’ll get autocomplete for nested fields and compile-time errors if relationships don’t exist.

Migration Workflows

Drizzle Kit simplifies migration management. Generate migrations automatically from schema changes:

npx drizzle-kit generate:pg
npx drizzle-kit push:pg

For production environments, create proper migration files:

npx drizzle-kit migrate

Claude Code helps you understand migration impact before execution. Ask Claude to review your pending schema changes and explain what each migration will do to your database structure.

Create custom migrations when you need data transformations:

// migrations/001_add_user_status.ts
import { sql } from 'drizzle-orm';
import { pgTable, varchar } from 'drizzle-orm/pg-core';

export const addUserStatus = async (db: any) => {
  await db.execute(sql`
    ALTER TABLE users 
    ADD COLUMN IF NOT EXISTS status varchar(20) DEFAULT 'active';
  `);
};

Transaction Support

When you need multiple operations to succeed or fail together, use transactions:

const createPostWithAuthor = async (postData: NewPost, userData: NewUser) => {
  return await db.transaction(async (tx) => {
    const [author] = await tx.insert(users).values(userData).returning();
    const [post] = await tx
      .insert(posts)
      .values({ ...postData, userId: author.id })
      .returning();
    return { author, post };
  });
};

Transactions ensure data consistency. If any operation fails, the entire transaction rolls back, preventing partial data states.

Advanced Query Patterns

Batch Operations

const createMultipleUsers = async (userList: NewUser[]) => {
  return await db.insert(users).values(userList).returning();
};

const updateMultiplePosts = async (ids: number[], published: boolean) => {
  return await db
    .update(posts)
    .set({ published, updatedAt: new Date() })
    .where(inArray(posts.id, ids))
    .returning();
};

Conditional Queries

const searchPosts = async (options: {
  authorId?: number;
  published?: boolean;
  limit?: number;
}) => {
  const conditions = [];
  
  if (options.authorId !== undefined) {
    conditions.push(eq(posts.userId, options.authorId));
  }
  if (options.published !== undefined) {
    conditions.push(eq(posts.published, options.published));
  }
  
  return db
    .select()
    .from(posts)
    .where(and(...conditions))
    .limit(options.limit || 10);
};

Aggregations

const getPostCountByUser = async () => {
  return db
    .select({
      userId: users.id,
      userName: users.name,
      postCount: count(posts.id),
    })
    .from(users)
    .leftJoin(posts, eq(users.id, posts.userId))
    .groupBy(users.id, users.name);
};

Claude Code excels at generating these complex queries. Describe your data needs, and Claude constructs the appropriate Drizzle query with correct types.

Cross-Database Compatibility

Drizzle supports PostgreSQL, MySQL, and SQLite with a consistent API. Switch databases by changing imports and configuration:

// PostgreSQL
import { pgTable, serial, varchar } from 'drizzle-orm/pg-core';

// MySQL
import { mysqlTable, int, varchar } from 'drizzle-orm/mysql-core';

// SQLite
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';

Your business logic remains largely the same across databases. Claude Code helps adapt queries when switching database backends by updating the necessary imports and adjusting dialect-specific syntax.

Testing Strategies

Write tests that use Drizzle’s type system to catch schema changes:

import { describe, it, expect, beforeAll } from 'vitest';
import { db } from './db';
import { users, posts } from './schema';

describe('Database Schema', () => {
  it('should have required user fields', async () => {
    const [user] = await db
      .insert(users)
      .values({
        email: 'test@example.com',
        name: 'Test User',
      })
      .returning();
    
    expect(user.email).toBe('test@example.com');
    expect(user.id).toBeDefined();
    expect(user.createdAt).toBeInstanceOf(Date);
  });
});

Use test databases for isolation:

import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

const testDb = async () => {
  const connection = postgres('postgres://localhost:5432/test_db');
  return drizzle(connection);
};

Performance Optimization

Use indexes for frequently queried columns:

export const posts = pgTable('posts', {
  // ... columns
  userId: integer('user_id').references(() => users.id).notNull(),
  published: boolean('published').default(false).notNull(),
}, (table) => ({
  userIdIdx: index('posts_user_id_idx').on(table.userId),
  publishedIdx: index('posts_published_idx').on(table.published),
}));

Analyze query performance with EXPLAIN:

const explainQuery = async () => {
  const result = await db.execute`
    EXPLAIN SELECT * FROM posts WHERE published = true
  `;
  console.log(result);
};

Claude Code can suggest indexes based on your query patterns. Share your access patterns with Claude, and it recommends appropriate indexing strategies.

Integrating with Claude Code Workflows

Start conversations with Claude by sharing your schema context:

I have a Drizzle schema with users and posts tables. 
The posts table has a foreign key to users. 
I need to create a query that gets all posts by a specific 
user email, including only published posts, sorted by creation date.

Claude generates the complete, typed query:

const getPostsByUserEmail = async (email: string) => {
  return db
    .select({
      id: posts.id,
      title: posts.title,
      content: posts.content,
      createdAt: posts.createdAt,
    })
    .from(posts)
    .innerJoin(users, eq(posts.userId, users.id))
    .where(and(
      eq(users.email, email),
      eq(posts.published, true)
    ))
    .orderBy(posts.createdAt);
};

Use Claude Code for debugging queries, explaining complex operations, or generating boilerplate for new schema additions.

Best Practices Summary

Keep your schema definitions in dedicated files and import them consistently. Use TypeScript’s type inference instead of explicit annotations when possible. Leverage Drizzle’s query builder for all database operations to maintain type safety. Write migrations for any schema changes rather than manually modifying tables. Test against databases that mirror your production environment.


Built by theluckystrike — More at zovo.one