Claude Code × Cloudflare D1:エッジSQLiteをWrangler CLIで自在に操るワークフロー

Claude Code × Cloudflare D1:エッジSQLiteをWrangler CLIで自在に操るワークフロー

はじめに

Cloudflare WorkersでAPIを作ろうとして、「データベースをどこに置けばいいか」で詰まった経験はないだろうか。TCPが使えないエッジ環境にNode.jsのDBドライバーを持ち込もうとすると、接続の壁にぶつかる。Neonや外部DBにHTTPで繋げる方法はあるが、Cloudflareのエコシステムの中で完結させたいケースも多い。

Cloudflare D1はその答えの一つだ。Cloudflareのエッジネットワーク上で動くマネージドSQLiteで、Workersからc.env.DBでアクセスできる。Wrangler CLIとClaude Codeを組み合わせると、DBの作成・マイグレーション・Honoとの接続まで一気通貫で進められる。


Cloudflare D1とは——エッジで動くマネージドSQLite

D1は2024年4月にGA(一般提供開始)を迎えたマネージドSQLiteサービスだ。GAと同時にデータベースサイズ上限が10GBに拡張され、1アカウントあたり最大50,000データベースが利用可能になった(上限は公式ドキュメントで最新値を確認してほしい)。

設計の核心は「SQLiteのSQL方言をそのまま使える」という点だ。Workersのコードからはc.env.DB.prepare('SELECT ...').bind(id).first()というシンプルな形でSQLが実行できる。エッジ環境でのDB接続を別途設計する必要がない。

Time Travel機能(過去30日以内の任意の時点にDBを復元)も提供されているため、マイグレーションの失敗時のリカバリーが取れる点もプロダクション利用の安心材料になる。

ローカル開発ではwrangler devがSQLiteのローカルインスタンスを自動起動する。本番と同じSQL方言でローカルでもテストできるため、「ローカルでは動いたのに本番で失敗した」というDB方言の差異問題が起きにくい。

料金は現行プランを公式(developers.cloudflare.com/d1/platform/pricing/)で確認してほしいが、Freeプランで相当量のリクエストを賄えるため、個人開発や小規模チームでの試験的利用から始めやすい。


D1プロジェクトのセットアップ

DBの作成とwrangler.tomlの設定

D1の運用はすべてWrangler CLIを介して行う。Claude CodeはBashツールでWranglerコマンドを直接実行できるため、DBの作成からwrangler.tomlの更新まで依頼できる。

# 1. D1データベースを作成(Cloudflareアカウントに登録される)
npx wrangler d1 create my-app-db

# 2. 出力されたdatabase_idをwrangler.tomlに記入
# wrangler.toml
name = "my-hono-app"
main = "src/index.ts"
compatibility_date = "2024-09-26"

[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
migrations_dir = "migrations"

binding = "DB"がコード内での参照名になる。Honoではc.env.DBでアクセスする。

マイグレーションの作成と適用

マイグレーションはSQLファイルで管理する:

# マイグレーションファイルを作成
npx wrangler d1 migrations create my-app-db "create_users_table"

# ローカルに適用(開発中)
npx wrangler d1 migrations apply my-app-db --local

# 本番環境に適用
npx wrangler d1 migrations apply my-app-db --remote

生成されるSQLファイルを直接編集する:

-- migrations/0001_create_users_table.sql
CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
);

CREATE INDEX idx_users_email ON users(email);

Claude Codeへの依頼例:

wrangler.tomlに記載のdatabase_idを使って、
usersテーブルを作成するマイグレーションを生成し、
ローカルに適用してください。
スキーマ: id TEXT PRIMARY KEY, email TEXT UNIQUE, created_at INTEGER

Claude Codeがマイグレーションファイルの生成からwrangler d1 migrations apply --localの実行まで行う。--local--remoteの切り替えをCLAUDE.mdに書いておくと、「本番に適用したら?」と聞いてからしか--remoteを実行しない運用が作れる。


Hono + D1でAPIを作る

生SQLパターン

D1の基本的なアクセスはprepare().bind().run()でのプリペアドステートメントだ:

// src/index.ts
import { Hono } from 'hono'

type Bindings = {
  DB: D1Database
}

const app = new Hono<{ Bindings: Bindings }>()

app.get('/users', async (c) => {
  const { results } = await c.env.DB.prepare(
    'SELECT * FROM users ORDER BY created_at DESC LIMIT 20'
  ).run()
  return c.json(results)
})

app.get('/users/:id', async (c) => {
  const id = c.req.param('id')
  const user = await c.env.DB.prepare(
    'SELECT * FROM users WHERE id = ?'
  ).bind(id).first()

  if (!user) return c.json({ error: 'Not found' }, 404)
  return c.json(user)
})

app.post('/users', async (c) => {
  const { email, name } = await c.req.json()
  const id = crypto.randomUUID()

  await c.env.DB.prepare(
    'INSERT INTO users (id, email, name, created_at) VALUES (?, ?, ?, ?)'
  ).bind(id, email, name, Date.now()).run()

  return c.json({ id, email, name }, 201)
})

export default app

prepare().bind()がSQLインジェクション対策の基本パターンだ。CLAUDE.mdに「生SQLを書く場合はprepare().bind()を使う」と書いておくと、Claude Codeが文字列結合でSQLを組み立てるコードを生成しなくなる。

Drizzle ORM + D1パターン

型安全なクエリが必要な場合はDrizzle ORMと組み合わせる:

// src/db/schema.ts
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'

export const users = sqliteTable('users', {
  id: text('id').primaryKey(),
  email: text('email').unique().notNull(),
  name: text('name').notNull(),
  createdAt: integer('created_at').notNull()
    .$defaultFn(() => Math.floor(Date.now() / 1000)),
})
// src/index.ts
import { drizzle } from 'drizzle-orm/d1'
import { users } from './db/schema'
import { eq } from 'drizzle-orm'

app.get('/users/:id', async (c) => {
  const db = drizzle(c.env.DB)
  const id = c.req.param('id')
  const result = await db.select().from(users).where(eq(users.id, id)).get()

  if (!result) return c.json({ error: 'Not found' }, 404)
  return c.json(result)
})

Drizzle Kitでマイグレーションを生成する場合の設定(driverプロパティ名と値は最新のDrizzle Kitの仕様を公式ドキュメントで確認してほしい):

// drizzle.config.ts
import type { Config } from 'drizzle-kit'

export default {
  schema: './src/db/schema.ts',
  out: './migrations',
  dialect: 'sqlite',
  driver: 'd1-http',  // 正確なプロパティ名はDrizzle Kit公式で確認すること
  dbCredentials: {
    accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
    databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
    token: process.env.CLOUDFLARE_D1_TOKEN!,
  },
} satisfies Config

Drizzleでマイグレーションファイルを生成し、Wranglerで適用するという2段階の流れになる:

npx drizzle-kit generate
npx wrangler d1 migrations apply DB --local

Claude Code × D1の実践ワークフロー

CLAUDE.mdへのガイドライン記述

## Cloudflare D1 ガイドライン

### Bindings
- DBバインディング名: DB(c.env.DB でアクセス)
- wrangler.tomlのd1_databases.binding = "DB"

### クエリ
- 生SQLの場合: prepare().bind().run() を使う(SQLインジェクション防止)
- ORMの場合: drizzle-orm/d1 を使う

### マイグレーション
- ファイル: migrations/NNNN_description.sql
- ローカル適用: npx wrangler d1 migrations apply DB --local
- 本番適用(確認後のみ): npx wrangler d1 migrations apply DB --remote

### ローカル開発
- npx wrangler dev でローカルSQLiteが自動起動
- .wrangler/state/v3/d1/ にローカルDBが保存される

「本番適用(確認後のみ)」という一文を入れておくことで、Claude Codeが--remoteを実行する前に確認を求めるパターンを意識させられる。


まとめ

D1がClaude Codeとの組み合わせで際立つのは、「Wrangler CLIのコマンドをClaude Codeが直接実行できる」という点だ。DBの作成・マイグレーションの生成・ローカル適用まで、会話の中で進められる。

試すならnpx wrangler d1 createからDBを作り、CLAUDE.mdにD1のガイドラインを書いてから「usersテーブルを作るマイグレーションを生成してローカルに適用して」と依頼してみてほしい。エッジ環境でのDB操作の体験が、想像より簡単だと気づくはずだ。

コメント

タイトルとURLをコピーしました