DEV Community

pipipi-dev
pipipi-dev

Posted on

Drizzle ORM Claude Code: Modern TypeScript Development

🎯 Article Overview

Problems This Article Solves

  • Type-safe database operations in TypeScript
  • Choosing the right ORM tool for your project
  • Understanding AI-assisted development compatibility

Target Readers

  • TypeScript experience: 1+ years
  • Basic database operation knowledge
  • Developers seeking efficient development tools

Prerequisites

  • TypeScript fundamentals
  • Basic SQL concepts (SELECT, JOIN, etc.)
  • Node.js project setup experience

πŸ“Š Conclusion & Key Points

Why We Recommend Drizzle ORM

  • βœ… Complete Type Safety: Detect SQL errors at compile-time
  • βœ… Intuitive SQL-like Syntax: Low learning curve
  • βœ… AI Development Compatibility: Explicit code that Claude Code understands easily
  • βœ… Lightweight Design: Minimal overhead

When working with databases in TypeScript projects, there are various options available such as Prisma, Supabase-js, and TypeORM. In this article, I'll explore the development experience with Drizzle ORM and its excellent compatibility with the AI-powered development tool Claude Code, based on actual project experience.

πŸ’‘ What is Drizzle ORM?

Drizzle ORM is a lightweight ORM (Object-Relational Mapping) tool designed with TypeScript-first principles.

Key Features

  • SQL-like Syntax: Intuitive API that leverages existing SQL knowledge
  • Complete Type Safety: Utilize TypeScript's type system for compile-time error detection
  • Lightweight Design: Minimal runtime overhead
  • Multi-database Support: Supports PostgreSQL, MySQL, and SQLite

What is ORM?
ORM (Object-Relational Mapping) is a technology that maps database tables to program objects. Instead of writing SQL directly, you can perform database operations using programming language syntax.

πŸ“Š Comparison of Major ORMs

Basic Query Syntax

// Drizzle - Intuitive SQL-like syntax
const users = await db
  .select({
    id: users.id,
    name: users.name,
    postCount: count(posts.id)
  })
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .where(eq(users.isActive, true))
  .groupBy(users.id);

// Prisma - Custom object notation
const users = await prisma.user.findMany({
  where: { isActive: true },
  include: {
    _count: {
      select: { posts: true }
    }
  }
});

// Supabase-js - Chain methods
const { data } = await supabase
  .from('users')
  .select(`
    id,
    name,
    posts(count)
  `)
  .eq('is_active', true);
Enter fullscreen mode Exit fullscreen mode

Type Safety Comparison

Feature Drizzle Prisma Supabase-js TypeORM
Compile-time type checking βœ… Complete βœ… Complete ⚠️ Partial ⚠️ Partial
Type generation from schema βœ… TypeScript definitions βœ… Auto-generated ⚠️ Manual/Generated βœ… Decorators
JOIN type inference βœ… Automatic βœ… Automatic ❌ Manual ⚠️ Partial
SQL query type safety βœ… Via builder ⚠️ Raw SQL unsupported ❌ String-based ⚠️ Partial
Runtime type validation ❌ None βœ… Available ❌ None ⚠️ Partial

πŸš€ Drizzle ORM Implementation Examples

1. Schema Definition

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

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  isActive: boolean('is_active').default(true),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

export const posts = pgTable('posts', {
  id: uuid('id').primaryKey().defaultRandom(),
  userId: uuid('user_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  content: text('content'),
  published: boolean('published').default(false),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});
Enter fullscreen mode Exit fullscreen mode

2. Complex Query Implementation

// Retrieve active users with post statistics
async function getActiveUsersWithStats() {
  const result = await db
    .select({
      userId: users.id,
      userName: users.name,
      email: users.email,
      totalPosts: count(posts.id),
      publishedPosts: count(
        case_().when(posts.published, 1).else(null)
      ),
      latestPostDate: max(posts.createdAt),
    })
    .from(users)
    .leftJoin(posts, eq(users.id, posts.userId))
    .where(eq(users.isActive, true))
    .groupBy(users.id)
    .having(gt(count(posts.id), 0))
    .orderBy(desc(count(posts.id)));

  return result;
}
Enter fullscreen mode Exit fullscreen mode

3. Transaction Handling

// Create user and welcome post simultaneously
async function createUserWithWelcomePost(userData: NewUser) {
  return await db.transaction(async (tx) => {
    // Create user
    const [newUser] = await tx
      .insert(users)
      .values(userData)
      .returning();

    // Create welcome post
    const [welcomePost] = await tx
      .insert(posts)
      .values({
        userId: newUser.id,
        title: 'Welcome to our platform!',
        content: `Hello ${newUser.name}, welcome aboard!`,
        published: true,
      })
      .returning();

    return { user: newUser, post: welcomePost };
  });
}
Enter fullscreen mode Exit fullscreen mode

πŸ€– Why Drizzle and Claude Code Work So Well Together

1. Explicit Code Generation

Claude Code can directly utilize SQL knowledge to generate Drizzle queries:

// Example instruction to Claude Code
"Write a query to fetch the latest 10 posts for a user"

// Generated code
const recentPosts = await db
  .select()
  .from(posts)
  .where(eq(posts.userId, userId))
  .orderBy(desc(posts.createdAt))
  .limit(10);
Enter fullscreen mode Exit fullscreen mode

2. Step-by-Step Implementation Support

// Step 1: Start with basic query
const allUsers = await db.select().from(users);

// Step 2: Add conditions
const activeUsers = await db
  .select()
  .from(users)
  .where(eq(users.isActive, true));

// Step 3: Add JOINs
const usersWithPosts = await db
  .select()
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .where(eq(users.isActive, true));

// Step 4: Add aggregation
const userStats = await db
  .select({
    user: users,
    postCount: count(posts.id)
  })
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .groupBy(users.id);
Enter fullscreen mode Exit fullscreen mode

3. Clear Error Messages

// Clear TypeScript type errors
db.select()
  .from(users)
  .where(eq(users.email, 123)); // ❌ Type error: number is not assignable to string

// Understandable SQL errors
db.select()
  .from(users)
  .where(eq(users.nonExistentColumn, 'value')); // ❌ Property 'nonExistentColumn' does not exist
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Use Cases Where Drizzle Particularly Excels

1. Complex JOINs

// Analytics with multiple table joins
const analytics = await db
  .select({
    date: sql<string>`DATE(${orders.createdAt})`,
    totalOrders: count(orders.id),
    uniqueCustomers: countDistinct(orders.customerId),
    totalRevenue: sum(orderItems.price),
    avgOrderValue: avg(orderItems.price),
  })
  .from(orders)
  .leftJoin(orderItems, eq(orders.id, orderItems.orderId))
  .leftJoin(customers, eq(orders.customerId, customers.id))
  .where(gte(orders.createdAt, lastMonth))
  .groupBy(sql`DATE(${orders.createdAt})`);
Enter fullscreen mode Exit fullscreen mode

2. Dynamic Query Building

function buildDynamicQuery(filters: FilterOptions) {
  let query = db.select().from(products);

  const conditions = [];

  if (filters.category) {
    conditions.push(eq(products.category, filters.category));
  }

  if (filters.minPrice) {
    conditions.push(gte(products.price, filters.minPrice));
  }

  if (filters.inStock) {
    conditions.push(gt(products.stock, 0));
  }

  if (conditions.length > 0) {
    query = query.where(and(...conditions));
  }

  if (filters.sortBy) {
    query = query.orderBy(
      filters.sortOrder === 'desc' 
        ? desc(products[filters.sortBy])
        : asc(products[filters.sortBy])
    );
  }

  return query;
}
Enter fullscreen mode Exit fullscreen mode

3. Raw SQL When Needed

// Advanced queries with window functions
const rankedProducts = await db.execute(sql`
  WITH RankedProducts AS (
    SELECT 
      *,
      ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) as rank
    FROM products
  )
  SELECT * FROM RankedProducts WHERE rank <= 5
`);
Enter fullscreen mode Exit fullscreen mode

🎯 Best Practices for Implementation

1. Project Setup

# Install required packages
npm install drizzle-orm postgres
npm install -D drizzle-kit @types/pg

# Create configuration file
touch drizzle.config.ts
Enter fullscreen mode Exit fullscreen mode
// drizzle.config.ts
import type { Config } from 'drizzle-kit';

export default {
  schema: './src/db/schema/*',
  out: './drizzle',
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
} satisfies Config;
Enter fullscreen mode Exit fullscreen mode

2. Connection Setup

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

const connectionString = process.env.DATABASE_URL!;
const sql = postgres(connectionString);

export const db = drizzle(sql, { schema });
Enter fullscreen mode Exit fullscreen mode

3. Migrations

# Generate migration files
npx drizzle-kit generate:pg

# Run migrations
npx drizzle-kit push:pg
Enter fullscreen mode Exit fullscreen mode

πŸš€ Summary

Drizzle ORM offers exceptional compatibility with AI-powered tools like Claude Code due to the following characteristics:

βœ… Intuitive SQL-like Syntax

  • Direct application of SQL knowledge
  • Predictable query generation

βœ… Complete Type Safety

  • Compile-time error detection
  • Maximum utilization of IDE autocompletion

βœ… Minimal Overhead

  • Thin wrapper layer
  • High-speed execution

βœ… Flexibility

  • Type-safe complex queries
  • Escape hatch to raw SQL

Next Steps

  1. Study basic concepts with Drizzle ORM Official Documentation
  2. Try it out in a small-scale project
  3. Experience the development workflow combined with Claude Code

Particularly for projects requiring complex JOINs and aggregation processing, adopting Drizzle ORM can lead to significant improvements in development efficiency.

As AI-assisted development becomes the norm, Drizzle ORM, which generates explicit and predictable code, will become a compelling choice for next-generation TypeScript development.

πŸ“š References

Top comments (0)