Step 1: Claude Code を使ったプロジェクトセットアップ
まず、Claude Code を活用して Drizzle ORM の初期セットアップを行いましょう。Claude Code に以下のプロンプトを投入することで、設定の煩雑さを一気に解消できます。
# Claude Code に渡すプロンプト(CLAUDE.md またはチャットで)
# 以下のような指示を与える
Claude Code へのプロンプト例:
新しい Node.js + TypeScript プロジェクトに Drizzle ORM + PostgreSQL をセットアップしてください。
要件:
- TypeScript 5.x、Node.js 20+
- drizzle-orm, drizzle-kit をインストール
- PostgreSQL接続設定(環境変数ベース)
- drizzle.config.ts の作成
- src/db/ ディレクトリにスキーマと接続設定を配置
- package.json にマイグレーション用スクリプトを追加
Claude Code が生成するプロジェクト構造:
my-app/
├── src/
│ └── db/
│ ├── index.ts ← DB接続
│ ├── schema/
│ │ ├── index.ts ← スキーマ集約
│ │ ├── users.ts
│ │ └── posts.ts
│ └── migrations/ ← 自動生成
├── drizzle.config.ts
├── .env
└── package.json
drizzle.config.ts
// drizzle.config.ts
import type { Config } from "drizzle-kit" ;
import * as dotenv from "dotenv" ;
dotenv. config ();
export default {
schema: "./src/db/schema/index.ts" ,
out: "./src/db/migrations" ,
dialect: "postgresql" ,
dbCredentials: {
url: process.env. DATABASE_URL ! ,
} ,
// マイグレーションファイルに詳細なメタデータを含める
verbose: true ,
strict: true ,
} satisfies Config ;
データベース接続設定
// src/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres" ;
import { Pool } from "pg" ;
import * as schema from "./schema" ;
// コネクションプールの設定(本番環境向け)
const pool = new Pool ({
connectionString: process.env. DATABASE_URL ,
max: 20 , // 最大接続数
idleTimeoutMillis: 30000 , // アイドル接続のタイムアウト
connectionTimeoutMillis: 2000 , // 接続タイムアウト
});
// スキーマを渡すことで型推論が完全に機能する
export const db = drizzle (pool, { schema });
// 接続テスト(起動時に実行)
export async function testConnection () {
try {
const result = await pool. query ( "SELECT 1" );
console. log ( "✅ Database connected successfully" );
return true ;
} catch (error) {
console. error ( "❌ Database connection failed:" , error);
return false ;
}
}
Step 2: Claude Code によるスキーマ設計の自動化
Drizzle ORM の最大の強みは TypeScript によるスキーマ定義です。Claude Code はこのスキーマ設計を劇的に加速します。
ユーザー・投稿システムのスキーマ設計
Claude Code へのプロンプト:
以下の要件でDrizzle ORMスキーマを設計してください:
- ユーザーテーブル(認証、プロフィール)
- 投稿テーブル(下書き・公開・アーカイブステータス)
- タグテーブルと多対多リレーション
- 全テーブルに created_at, updated_at を追加
- ソフトデリート(deleted_at)を実装
- インデックスはクエリパターンを考慮して設計
Claude Code が生成するスキーマ例:
// src/db/schema/users.ts
import {
pgTable,
uuid,
varchar,
text,
timestamp,
boolean,
pgEnum,
} from "drizzle-orm/pg-core" ;
// ユーザーロールのEnum型
export const userRoleEnum = pgEnum ( "user_role" , [ "admin" , "editor" , "viewer" ]);
export const users = pgTable ( "users" , {
id: uuid ( "id" ). primaryKey (). defaultRandom (),
email: varchar ( "email" , { length: 255 }). notNull (). unique (),
displayName: varchar ( "display_name" , { length: 100 }). notNull (),
avatarUrl: text ( "avatar_url" ),
role: userRoleEnum ( "role" ). notNull (). default ( "viewer" ),
emailVerified: boolean ( "email_verified" ). notNull (). default ( false ),
createdAt: timestamp ( "created_at" , { withTimezone: true })
. notNull ()
. defaultNow (),
updatedAt: timestamp ( "updated_at" , { withTimezone: true })
. notNull ()
. defaultNow ()
. $onUpdate (() => new Date ()),
deletedAt: timestamp ( "deleted_at" , { withTimezone: true }),
});
// 型エクスポート(TypeScript での利用に便利)
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
// src/db/schema/posts.ts
import {
pgTable,
uuid,
varchar,
text,
timestamp,
integer,
pgEnum,
index,
} from "drizzle-orm/pg-core" ;
import { users } from "./users" ;
export const postStatusEnum = pgEnum ( "post_status" , [
"draft" ,
"published" ,
"archived" ,
]);
export const posts = pgTable (
"posts" ,
{
id: uuid ( "id" ). primaryKey (). defaultRandom (),
title: varchar ( "title" , { length: 500 }). notNull (),
slug: varchar ( "slug" , { length: 500 }). notNull (). unique (),
content: text ( "content" ),
excerpt: text ( "excerpt" ),
authorId: uuid ( "author_id" )
. notNull ()
. references (() => users.id, { onDelete: "cascade" }),
status: postStatusEnum ( "status" ). notNull (). default ( "draft" ),
viewCount: integer ( "view_count" ). notNull (). default ( 0 ),
publishedAt: timestamp ( "published_at" , { withTimezone: true }),
createdAt: timestamp ( "created_at" , { withTimezone: true })
. notNull ()
. defaultNow (),
updatedAt: timestamp ( "updated_at" , { withTimezone: true })
. notNull ()
. defaultNow ()
. $onUpdate (() => new Date ()),
deletedAt: timestamp ( "deleted_at" , { withTimezone: true }),
},
( table ) => ({
// クエリパターンに基づいたインデックス設計
authorIdIdx: index ( "posts_author_id_idx" ). on (table.authorId),
statusIdx: index ( "posts_status_idx" ). on (table.status),
slugIdx: index ( "posts_slug_idx" ). on (table.slug),
// 公開記事の一覧取得に最適化した複合インデックス
statusPublishedAtIdx: index ( "posts_status_published_at_idx" ). on (
table.status,
table.publishedAt
),
})
);
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
// src/db/schema/tags.ts — 多対多リレーション
import { pgTable, uuid, varchar, primaryKey } from "drizzle-orm/pg-core" ;
import { posts } from "./posts" ;
export const tags = pgTable ( "tags" , {
id: uuid ( "id" ). primaryKey (). defaultRandom (),
name: varchar ( "name" , { length: 100 }). notNull (). unique (),
slug: varchar ( "slug" , { length: 100 }). notNull (). unique (),
});
// 中間テーブル(多対多)
export const postTags = pgTable (
"post_tags" ,
{
postId: uuid ( "post_id" )
. notNull ()
. references (() => posts.id, { onDelete: "cascade" }),
tagId: uuid ( "tag_id" )
. notNull ()
. references (() => tags.id, { onDelete: "cascade" }),
},
( table ) => ({
pk: primaryKey ({ columns: [table.postId, table.tagId] }),
})
);
export type Tag = typeof tags.$inferSelect;
Step 3: マイグレーション戦略と自動化
マイグレーションは本番環境での最大のリスクポイントです。Claude Code と Drizzle Kit を組み合わせることで、安全で再現性の高いマイグレーション戦略を構築できます。
マイグレーションコマンドの設定
// package.json
{
"scripts" : {
"db:generate" : "drizzle-kit generate" ,
"db:migrate" : "drizzle-kit migrate" ,
"db:push" : "drizzle-kit push" ,
"db:studio" : "drizzle-kit studio" ,
"db:check" : "drizzle-kit check" ,
"db:drop" : "drizzle-kit drop"
}
}
マイグレーションワークフロー
Drizzle Kit のマイグレーションは2段階に分かれます。
1. マイグレーションファイルの生成 :
npm run db:generate
# → src/db/migrations/0001_xxxxx.sql が自動生成される
2. マイグレーションの適用 :
npm run db:migrate
# → 未適用のマイグレーションが順番に実行される
本番環境向けマイグレーション実行スクリプト
// scripts/migrate.ts
import { drizzle } from "drizzle-orm/node-postgres" ;
import { migrate } from "drizzle-orm/node-postgres/migrator" ;
import { Pool } from "pg" ;
async function runMigration () {
console. log ( "🔄 Starting database migration..." );
const pool = new Pool ({
connectionString: process.env. DATABASE_URL ,
max: 1 , // マイグレーション中は接続数を最小限に
});
const db = drizzle (pool);
try {
await migrate (db, {
migrationsFolder: "./src/db/migrations" ,
});
console. log ( "✅ Migration completed successfully" );
} catch (error) {
console. error ( "❌ Migration failed:" , error);
process. exit ( 1 );
} finally {
await pool. end ();
}
}
runMigration ();
# 本番環境での実行
DATABASE_URL = "postgresql://..." npx tsx scripts/migrate.ts
Claude Code によるマイグレーション安全チェック
Claude Code に以下のプロンプトを与えることで、マイグレーションファイルのリスク評価を自動化できます。
src/db/migrations/ ディレクトリの最新マイグレーションファイルを確認して、
以下の観点でリスク評価してください:
1. データロスのリスクがあるカラム削除や型変更
2. ロングランニングトランザクションになりうるテーブルロック操作
3. 既存データへの影響(デフォルト値の変更など)
4. ロールバック手順の必要性
問題があれば修正提案も含めて報告してください。
Step 4: 高度なクエリパターン
Drizzle ORM の真価は型安全なクエリ記述にあります。Claude Code はこれらの複雑なクエリパターンを自然言語から生成できます。
リレーションクエリ(JOIN を使った複合取得)
// src/db/queries/posts.ts
import { db } from "../index" ;
import { posts, users, postTags, tags } from "../schema" ;
import { and, eq, isNull, desc, sql } from "drizzle-orm" ;
// 公開済み投稿をタグ・著者情報付きで取得
export async function getPublishedPosts ( limit = 10 , offset = 0 ) {
const result = await db
. select ({
id: posts.id,
title: posts.title,
slug: posts.slug,
excerpt: posts.excerpt,
publishedAt: posts.publishedAt,
viewCount: posts.viewCount,
author: {
id: users.id,
displayName: users.displayName,
avatarUrl: users.avatarUrl,
},
})
. from (posts)
. innerJoin (users, eq (posts.authorId, users.id))
. where (
and (
eq (posts.status, "published" ),
isNull (posts.deletedAt),
isNull (users.deletedAt)
)
)
. orderBy ( desc (posts.publishedAt))
. limit (limit)
. offset (offset);
return result;
// 戻り値の型は TypeScript が自動推論 — キャスト不要
}
// 閲覧数でランキングされた人気記事の取得
export async function getPopularPosts ( days = 30 , limit = 5 ) {
const cutoffDate = new Date ();
cutoffDate. setDate (cutoffDate. getDate () - days);
return await db
. select ({
id: posts.id,
title: posts.title,
slug: posts.slug,
viewCount: posts.viewCount,
publishedAt: posts.publishedAt,
})
. from (posts)
. where (
and (
eq (posts.status, "published" ),
isNull (posts.deletedAt),
sql `${ posts . publishedAt } >= ${ cutoffDate . toISOString () }`
)
)
. orderBy ( desc (posts.viewCount))
. limit (limit);
}
トランザクション処理
// src/db/queries/transactions.ts
import { db } from "../index" ;
import { posts, postTags, tags } from "../schema" ;
import { eq, inArray } from "drizzle-orm" ;
// 投稿作成とタグ紐付けをトランザクションで実行
export async function createPostWithTags (
postData : {
title : string ;
slug : string ;
content : string ;
authorId : string ;
},
tagNames : string []
) {
return await db. transaction ( async ( tx ) => {
// 1. 投稿を作成
const [ newPost ] = await tx
. insert (posts)
. values (postData)
. returning ();
// 2. タグをupsert(存在しない場合は新規作成)
const upsertedTags = await Promise . all (
tagNames. map (( name ) =>
tx
. insert (tags)
. values ({
name,
slug: name. toLowerCase (). replace ( / \s + / g , "-" ),
})
. onConflictDoUpdate ({
target: tags.slug,
set: { name },
})
. returning ()
)
);
// 3. 投稿とタグを紐付け
const tagIds = upsertedTags. map (([ tag ]) => tag.id);
if (tagIds. length > 0 ) {
await tx. insert (postTags). values (
tagIds. map (( tagId ) => ({
postId: newPost.id,
tagId,
}))
);
}
return { post: newPost, tagCount: tagIds. length };
// どこかでエラーが発生した場合、トランザクション全体がロールバックされる
});
}
ページネーション付きクエリ(カーソルベース)
// カーソルベースのページネーション(無限スクロール対応)
export async function getPostsCursor (
cursor ?: string ,
limit = 10
) {
const query = db
. select ({
id: posts.id,
title: posts.title,
slug: posts.slug,
publishedAt: posts.publishedAt,
})
. from (posts)
. where (
and (
eq (posts.status, "published" ),
isNull (posts.deletedAt),
cursor
? sql `${ posts . publishedAt } < ${ new Date ( cursor ). toISOString () }`
: undefined
)
)
. orderBy ( desc (posts.publishedAt))
. limit (limit + 1 ); // 次のページ存在チェック用に1件多く取得
const results = await query;
const hasNextPage = results. length > limit;
const items = hasNextPage ? results. slice ( 0 , - 1 ) : results;
const nextCursor = hasNextPage
? items[items. length - 1 ].publishedAt?. toISOString ()
: null ;
return { items, nextCursor, hasNextPage };
}
Step 5: Claude Code カスタムフックによる開発ワークフロー自動化
Claude Code のフック機能を使うことで、データベース関連の作業を自動化できます。これは特にチーム開発で威力を発揮します。
CLAUDE.md によるプロジェクトルール定義
<!-- CLAUDE.md -->
# データベース開発ルール
## Drizzle ORM の規約
- スキーマ変更後は必ず `npm run db:generate` でマイグレーションファイルを生成すること
- `db.query.*` の関連クエリ API より `db.select().from()` の明示的なクエリを優先すること
- すべてのクエリ関数はエラーハンドリングを含めること
- N+1クエリが発生しないよう、JOIN を活用すること
## 命名規約
- テーブル名: スネークケース複数形(users, posts, post_tags)
- カラム名: スネークケース(created_at, author_id)
- TypeScript 変数: キャメルケース(createdAt, authorId)
- インデックス: `{tableName}_{columnName}_idx` パターン
## セキュリティ
- ユーザー入力を直接クエリに挿入しない(drizzle の型安全API を使う)
- `sql` テンプレートリテラルを使う場合は必ずパラメータ化する
PreToolUse フックによる自動チェック
// .claude/settings.json
{
"hooks" : {
"PreToolUse" : [
{
"matcher" : "Write|Edit" ,
"hooks" : [
{
"type" : "command" ,
"command" : "bash -c \" if echo '$TOOL_ARGS' | grep -q 'schema.* \\ .ts'; then echo '⚠️ スキーマ変更を検出。npm run db:generate を実行してください'; fi \" "
}
]
}
]
}
}
PostToolUse フックによるマイグレーション自動提案
#!/bin/bash
# .claude/hooks/post-schema-change.sh
# スキーマファイルが変更された場合にマイグレーション生成を促す
CHANGED_FILE = " $1 "
if [[ " $CHANGED_FILE " == * "/schema/" * && " $CHANGED_FILE " == * ".ts" ]]; then
echo ""
echo "📦 スキーマ変更を検出しました。"
echo "以下のコマンドでマイグレーションファイルを生成してください:"
echo " npm run db:generate"
echo ""
echo "生成後のマイグレーションファイルを確認し、問題なければ:"
echo " npm run db:migrate"
fi
Step 6: パフォーマンス最適化 — インデックス設計とクエリ分析
本番環境でのデータベースパフォーマンスは、適切なインデックス設計と遅いクエリの特定にかかっています。Claude Code はクエリの分析と最適化提案を行うことができます。
EXPLAIN ANALYZE を使ったクエリ分析
// src/db/utils/explain.ts
import { db } from "../index" ;
import { sql } from "drizzle-orm" ;
// クエリの実行計画を取得するユーティリティ
export async function explainQuery ( queryStr : string ) {
const result = await db. execute (
sql `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${ sql . raw ( queryStr ) }`
);
return result.rows[ 0 ];
}
// 使用例
const plan = await explainQuery ( `
SELECT p.*, u.display_name
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.status = 'published'
ORDER BY p.published_at DESC
LIMIT 10
` );
console. log ( JSON . stringify (plan, null , 2 ));
Claude Code によるクエリ最適化
実行計画を Claude Code に貼り付けて、以下のプロンプトで最適化提案を依頼できます。
以下のPostgreSQLクエリ実行計画を分析して、パフォーマンス改善の提案をしてください:
[EXPLAIN ANALYZE の出力]
特に確認してほしい点:
1. Sequential Scan が発生している箇所
2. 不足しているインデックスの特定
3. クエリの書き換えによる最適化
4. 統計情報の更新が必要かどうか
部分インデックス(Partial Index)の活用
// src/db/schema/posts.ts — 高度なインデックス設計
import { pgTable, ..., sql as pgSql } from "drizzle-orm/pg-core" ;
export const posts = pgTable (
"posts" ,
{
// ... フィールド定義
},
( table ) => ({
// 公開済み記事のみを対象とした部分インデックス
// WHERE status = 'published' のクエリが劇的に高速化
publishedPostsIdx: index ( "posts_published_idx" )
. on (table.publishedAt)
. where ( pgSql `status = 'published'` ),
// ソフトデリート対応の部分インデックス
activePostsIdx: index ( "posts_active_idx" )
. on (table.authorId, table.createdAt)
. where ( pgSql `deleted_at IS NULL` ),
})
);
Step 7: エッジ環境での Drizzle ORM — Cloudflare Workers 対応
Drizzle ORM の軽量さは、Cloudflare Workers などのエッジ環境での動作を可能にします。これは Prisma では困難だった領域です。
Cloudflare Workers + Drizzle + Neon/Hyperdrive
// src/db/edge.ts — Cloudflare Workers 向け設定
import { drizzle } from "drizzle-orm/neon-http" ;
import { neon } from "@neondatabase/serverless" ;
import * as schema from "./schema" ;
// Cloudflare Workers での環境変数アクセス
export function createDb ( env : { DATABASE_URL : string }) {
const sql = neon (env. DATABASE_URL );
return drizzle (sql, { schema });
}
// Cloudflare Workers のハンドラー
export default {
async fetch ( request : Request , env : { DATABASE_URL : string }) {
const db = createDb (env);
// エッジで直接データベースクエリを実行
const latestPosts = await db
. select ({
id: schema.posts.id,
title: schema.posts.title,
slug: schema.posts.slug,
})
. from (schema.posts)
. where ( eq (schema.posts.status, "published" ))
. limit ( 10 );
return Response. json ({ posts: latestPosts });
} ,
} ;
Hyperdrive によるコネクションプーリング(Cloudflare 環境)
// Cloudflare Hyperdrive を使った本番グレードの接続
import { drizzle } from "drizzle-orm/node-postgres" ;
import { Pool } from "pg" ;
export function createDbWithHyperdrive (
env : { HYPERDRIVE : Hyperdrive }
) {
// Hyperdrive が自動的にコネクションプーリングを処理
const pool = new Pool ({
connectionString: env. HYPERDRIVE .connectionString,
});
return drizzle (pool, { schema });
}
Step 8: よくあるエラーと対処法
エラー1: column "X" does not exist
原因: スキーマを変更したがマイグレーションを実行していない
対処: npm run db:generate && npm run db:migrate
エラー2: too many connections
// 対処: コネクションプールの設定を見直す
const pool = new Pool ({
connectionString: process.env. DATABASE_URL ,
max: 10 , // デフォルト10に下げる
idleTimeoutMillis: 10000 ,
connectionTimeoutMillis: 3000 ,
});
エラー3: マイグレーションの競合
# 対処: マイグレーション履歴を確認
npm run db:check
# 競合するマイグレーションファイルを削除してやり直す
npm run db:drop
エラー4: 型エラー Type 'string' is not assignable
// 原因: pgEnum の値が TypeScript に認識されていない
// 対処: as const を使って型を絞り込む
const status = "published" as const ;
// または
type PostStatus = typeof postStatusEnum.enumValues[number];
ここまでの要点
Claude Code と Drizzle ORM の組み合わせは、TypeScript バックエンド開発における強力なデュオです。本記事で解説した内容を振り返ると:
型安全なスキーマ設計 : TypeScript コードとしてのスキーマ定義により、IDE の補完と型推論が完全に機能
Claude Code による自動化 : スキーマ生成、クエリ最適化、マイグレーションリスク分析を AI が支援
本番対応のコネクション管理 : プールサイズ、タイムアウト、Hyperdrive の適切な設定
エッジ環境の対応 : Cloudflare Workers でも動作する軽量アーキテクチャ
高度なクエリパターン : トランザクション、カーソルページネーション、部分インデックスの活用
Drizzle ORM はまだ比較的新しいライブラリですが、そのシンプルさと型安全性により急速に採用が広がっています。Claude Code との組み合わせで、データベース設計の試行錯誤時間を大幅に削減し、本番品質のコードを短時間で実装できます。
TypeScript バックエンド開発の基礎をさらに固めたい方には、Claude Code × Node.js / TypeScript バックエンド開発完全ガイド も参照ください。また、データベースを活用した本格的なECサイト構築にはClaude Codeで本格ECサイトを構築 — Next.js + Stripe + VPS 実践ガイドが参考になります。