Tips & Tricks (अपडेट: 2/6/2026)

Claude Code के साथ Drizzle ORM: PostgreSQL, migration, Zod और CI

Claude Code से Drizzle ORM लागू करें: typed schema, migration, seed, transaction, Zod validation और CI checks.

Claude Code के साथ Drizzle ORM: PostgreSQL, migration, Zod और CI

Drizzle ORM एक TypeScript-first ORM है। ORM वह परत है जो application code और database operations को जोड़ती है। Drizzle की खासियत यह है कि इसका query style SQL के बहुत करीब रहता है, इसलिए Claude Code schema पढ़ सकता है, relation समझ सकता है, migration बना सकता है और index का कारण समझा सकता है।

Database layer में गलती अक्सर चुपचाप होती है। schema.ts compile हो सकता है, लेकिन generated migration किसी column को drop कर सकती है। seed पहली बार चल सकता है और दूसरी बार unique constraint पर fail हो सकता है। transaction के अंदर email, Stripe या webhook call करने से database connection देर तक hold रह सकता है। इसलिए Claude Code को सिर्फ “Drizzle लगाओ” न कहें। उससे schema, migration, query, transaction, seed, Zod validation और CI एक साथ मांगें।

Official references के लिए Drizzle ORM docs, Drizzle Kit docs, Transactions, Drizzle Zod docs और Claude Code docs देखें। तुलना के लिए internal guides Prisma ORM, database migration और Zod validation उपयोगी हैं।

Workflow

Claude Code को prompt में business rule भी दें, सिर्फ table names नहीं।

Drizzle ORM से blog API की database layer बनाइए।

Stack:
- PostgreSQL
- drizzle-orm, drizzle-kit, node-postgres
- tables: User, Post, Category, Comment, AuditLog
- Post status: draft, published, archived
- email और slug unique
- list query status, publishedAt, author, category, search से filter होगी
- Post delete पर सिर्फ Comment और join table cascade हों
- User delete पर Post delete नहीं होना चाहिए

schema, drizzle.config.ts, query, transaction, seed, Zod और CI दीजिए।
flowchart LR
  A["Prompt"] --> B["schema.ts"]
  B --> C["drizzle-kit generate"]
  C --> D["SQL review"]
  D --> E["query, transaction, seed"]
  E --> F["Zod validation"]
  F --> G["CI checks"]

Schema और migration

नीचे का schema real project के review points दिखाता है: unique, index, relation और delete rules।

// db/schema.ts
import { relations } from "drizzle-orm";
import {
  boolean,
  index,
  integer,
  jsonb,
  pgEnum,
  pgTable,
  primaryKey,
  text,
  timestamp,
  uniqueIndex,
  uuid,
  varchar,
} from "drizzle-orm/pg-core";

export const postStatus = pgEnum("post_status", ["draft", "published", "archived"]);

export const users = pgTable(
  "users",
  {
    id: uuid("id").defaultRandom().primaryKey(),
    email: varchar("email", { length: 255 }).notNull(),
    name: varchar("name", { length: 120 }).notNull(),
    role: varchar("role", { length: 40 }).default("editor").notNull(),
    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
  },
  (table) => [
    uniqueIndex("users_email_unique").on(table.email),
    index("users_role_idx").on(table.role),
  ],
);

export const categories = pgTable(
  "categories",
  {
    id: uuid("id").defaultRandom().primaryKey(),
    slug: varchar("slug", { length: 120 }).notNull(),
    name: varchar("name", { length: 120 }).notNull(),
  },
  (table) => [uniqueIndex("categories_slug_unique").on(table.slug)],
);

export const posts = pgTable(
  "posts",
  {
    id: uuid("id").defaultRandom().primaryKey(),
    slug: varchar("slug", { length: 160 }).notNull(),
    title: varchar("title", { length: 160 }).notNull(),
    body: text("body").notNull(),
    status: postStatus("status").default("draft").notNull(),
    authorId: uuid("author_id").notNull().references(() => users.id, { onDelete: "restrict" }),
    viewCount: integer("view_count").default(0).notNull(),
    featured: boolean("featured").default(false).notNull(),
    publishedAt: timestamp("published_at", { withTimezone: true }),
    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
  },
  (table) => [
    uniqueIndex("posts_slug_unique").on(table.slug),
    index("posts_status_published_at_idx").on(table.status, table.publishedAt),
    index("posts_author_id_idx").on(table.authorId),
  ],
);

export const postCategories = pgTable(
  "post_categories",
  {
    postId: uuid("post_id").notNull().references(() => posts.id, { onDelete: "cascade" }),
    categoryId: uuid("category_id").notNull().references(() => categories.id, { onDelete: "cascade" }),
  },
  (table) => [primaryKey({ columns: [table.postId, table.categoryId] })],
);

export const comments = pgTable(
  "comments",
  {
    id: uuid("id").defaultRandom().primaryKey(),
    postId: uuid("post_id").notNull().references(() => posts.id, { onDelete: "cascade" }),
    authorId: uuid("author_id").notNull().references(() => users.id, { onDelete: "restrict" }),
    body: text("body").notNull(),
    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  },
  (table) => [
    index("comments_post_created_at_idx").on(table.postId, table.createdAt),
    index("comments_author_id_idx").on(table.authorId),
  ],
);

export const auditLogs = pgTable(
  "audit_logs",
  {
    id: uuid("id").defaultRandom().primaryKey(),
    action: varchar("action", { length: 80 }).notNull(),
    targetId: uuid("target_id").notNull(),
    metadata: jsonb("metadata").$type<Record<string, unknown>>(),
    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  },
  (table) => [index("audit_logs_action_created_at_idx").on(table.action, table.createdAt)],
);

export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), comments: many(comments) }));
export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, { fields: [posts.authorId], references: [users.id] }),
  comments: many(comments),
  categories: many(postCategories),
}));
export const categoriesRelations = relations(categories, ({ many }) => ({ posts: many(postCategories) }));
export const postCategoriesRelations = relations(postCategories, ({ one }) => ({
  post: one(posts, { fields: [postCategories.postId], references: [posts.id] }),
  category: one(categories, { fields: [postCategories.categoryId], references: [categories.id] }),
}));
export const commentsRelations = relations(comments, ({ one }) => ({
  post: one(posts, { fields: [comments.postId], references: [posts.id] }),
  author: one(users, { fields: [comments.authorId], references: [users.id] }),
}));
// drizzle.config.ts
import "dotenv/config";
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: { url: process.env.DATABASE_URL! },
  verbose: true,
  strict: true,
});
npm run db:generate
npm run db:check
npm run db:migrate

Generated SQL में अनचाहा DROP, existing data पर खतरनाक NOT NULL, बहुत broad cascade और missing index देखें। CI pass होना production data safety का proof नहीं है।

Query, seed और validation

// db/client.ts
import "dotenv/config";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";

export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
// db/posts.ts
import { and, desc, eq, ilike, sql } from "drizzle-orm";
import { z } from "zod";
import { db } from "./client";
import { auditLogs, categories, comments, postCategories, posts, users } from "./schema";
import { createPostInputSchema } from "./validation";

type CreatePostInput = z.infer<typeof createPostInputSchema>;

export async function createPost(input: CreatePostInput) {
  const data = createPostInputSchema.parse(input);

  return db.transaction(async (tx) => {
    const [post] = await tx.insert(posts).values({
      slug: data.slug,
      title: data.title,
      body: data.body,
      authorId: data.authorId,
    }).returning();

    for (const slug of data.categorySlugs) {
      const [category] = await tx.insert(categories)
        .values({ slug, name: slug })
        .onConflictDoUpdate({ target: categories.slug, set: { name: slug } })
        .returning();
      await tx.insert(postCategories)
        .values({ postId: post.id, categoryId: category.id })
        .onConflictDoNothing();
    }

    await tx.insert(auditLogs).values({
      action: "post.create",
      targetId: post.id,
      metadata: { slug: post.slug },
    });

    return post;
  });
}

export async function listPublishedPosts(params: { page?: number; perPage?: number; search?: string } = {}) {
  const page = Math.max(params.page ?? 1, 1);
  const perPage = Math.min(Math.max(params.perPage ?? 20, 1), 50);
  const where = params.search
    ? and(eq(posts.status, "published"), ilike(posts.title, `%${params.search}%`))
    : eq(posts.status, "published");

  const [items, [{ total }]] = await Promise.all([
    db.select({
      id: posts.id,
      slug: posts.slug,
      title: posts.title,
      publishedAt: posts.publishedAt,
      authorName: users.name,
      commentCount: sql<number>`count(${comments.id})::int`,
    })
      .from(posts)
      .innerJoin(users, eq(posts.authorId, users.id))
      .leftJoin(comments, eq(comments.postId, posts.id))
      .where(where)
      .groupBy(posts.id, posts.slug, posts.title, posts.publishedAt, users.name)
      .orderBy(desc(posts.publishedAt), desc(posts.createdAt))
      .limit(perPage)
      .offset((page - 1) * perPage),
    db.select({ total: sql<number>`count(*)::int` }).from(posts).where(where),
  ]);

  return { items, pagination: { page, perPage, total, totalPages: Math.ceil(total / perPage) } };
}
// db/validation.ts
import { createInsertSchema } from "drizzle-orm/zod";
import { z } from "zod";
import { posts } from "./schema";

export const createPostInputSchema = createInsertSchema(posts, {
  slug: (schema) => schema.min(3).max(160).regex(/^[a-z0-9-]+$/),
  title: (schema) => schema.min(1).max(160),
  body: (schema) => schema.min(50),
})
  .pick({ slug: true, title: true, body: true, authorId: true })
  .extend({
    categorySlugs: z.array(z.string().min(1).max(120)).min(1).max(5),
  });
// db/seed.ts
import { db, pool } from "./client";
import { categories, postCategories, posts, users } from "./schema";

async function main() {
  const [user] = await db.insert(users)
    .values({ email: "masa@example.com", name: "Masa", role: "admin" })
    .onConflictDoUpdate({ target: users.email, set: { name: "Masa", role: "admin", updatedAt: new Date() } })
    .returning();

  const [category] = await db.insert(categories)
    .values({ slug: "drizzle", name: "Drizzle ORM" })
    .onConflictDoUpdate({ target: categories.slug, set: { name: "Drizzle ORM" } })
    .returning();

  const [post] = await db.insert(posts)
    .values({
      slug: "claude-code-drizzle-demo",
      title: "Claude Code Drizzle demo",
      body: "A seeded post for local verification.",
      status: "published",
      authorId: user.id,
      publishedAt: new Date(),
    })
    .onConflictDoUpdate({ target: posts.slug, set: { title: "Claude Code Drizzle demo", updatedAt: new Date() } })
    .returning();

  await db.insert(postCategories).values({ postId: post.id, categoryId: category.id }).onConflictDoNothing();
}

main().finally(async () => pool.end());

CI, use cases और CTA

name: drizzle
on:
  pull_request:
jobs:
  db:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: app
          POSTGRES_PASSWORD: app
          POSTGRES_DB: app_test
        ports:
          - 5432:5432
    env:
      DATABASE_URL: postgresql://app:app@localhost:5432/app_test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run db:generate
      - run: npm run db:check
      - run: npm run db:migrate
      - run: npm run db:seed
      - run: npm run typecheck
      - run: npm test

Review checklist

PR में Claude Code से पहले self-review कराएं। हर index के लिए पूछें कि वह कौन से page या API query को support करता है। हर onDelete के लिए business reason लिखवाएं। Migration SQL में DROP, broad cascade और risky NOT NULL देखें। Seed को दो बार चलाकर देखें। Zod validation को database constraint का replacement न मानें। CI result के साथ limitation भी लिखें: empty test database पर migrate होना production data safety की guarantee नहीं है।

तीन खास use cases हैं। CMS में slug, publish status और publishedAt index सबसे जरूरी हैं। SaaS admin में tenant, role और audit log देखते हैं। Course platform में progress update और access rule को transaction में consistent रखते हैं। Revenue content में CTA click और product path के लिए analytics table जोड़ना हो सकता है, लेकिन existing constraints कमजोर नहीं होने चाहिए।

Common mistakes: migration SQL न पढ़ना, seed को idempotent न बनाना, transaction में external API call करना, Zod को DB constraint का replacement समझना, pagination limit भूलना, और Claude Code को unrelated migration edit करने देना। Team rollout के लिए English Claude Code training and consultation देखें। आगे पढ़ने के लिए Supabase integration और CI/CD setup अच्छे next links हैं।

एक छोटे blog API test में सबसे अच्छा result तब मिला जब Claude Code से generated SQL explain कराया गया। पहले draft में delete rule ज्यादा aggressive था। onDelete, index और seed idempotency की अलग review कराने के बाद diff approve करना आसान हुआ। Drizzle ORM हल्का है, लेकिन safety Claude Code की speed, human SQL review और CI evidence को साथ रखने से आती है।

#Claude Code #Drizzle ORM #database #TypeScript #SQL
मुफ़्त

मुफ़्त PDF: Claude Code cheatsheet

Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.

हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.

Masa

लेखक के बारे में

Masa

Claude Code workflow और team adoption पर काम करने वाला engineer.