※本記事には広告(A8・楽天アフィリエイト)リンクが含まれる場合があります。

「Cloudflare Workersでアプリを作りたいけど、データをどこに保存すればいいの?」「KVだと検索ができなくて困る。でもRDSは高すぎる」——個人開発者からよく聞く悩みです。

その答えが Cloudflare D1 です。Cloudflareのエッジネットワーク上で動くSQLiteデータベースで、Workersからゼロレイテンシに近い速度でクエリできます。しかも無料枠が非常に充実しており、個人開発のマイクロSaaSなら月$0で本番運用できます。

この記事では、D1の基本概念からWorkersへの組み込み、Honoとの連携、マイグレーション管理、そして実際のSaaS構築パターンまで、実例コードを交えながら徹底解説します。

Cloudflare D1とは?

D1はCloudflareが提供するサーバーレスSQLiteデータベースです。2023年に正式GA(Generally Available)となり、2026年現在では多くの個人開発者・スタートアップが本番環境で採用しています。

D1の主な特徴

  • SQLite互換: 標準的なSQL構文がそのまま使える。既存のSQLiteスキーマを移植しやすい
  • Workersネイティブ: Workers内から env.DB.prepare() で直接クエリ。外部接続不要
  • エッジ分散読み取り: 世界中のCloudflareエッジで読み取りキャッシュが効く(レプリカ自動展開)
  • wrangler連携: CLIからマイグレーション管理・ローカル開発が完結
  • 寛大な無料枠: 読み取り500万行/日、書き込み10万行/日、容量5GB(2026年4月時点)

KV・R2・Durable Objectsとの使い分け

ストレージ 向いているユースケース SQLクエリ
D1 ユーザーデータ、記事、設定など構造化データ ✅ 可能
KV セッション、キャッシュ、フラグなどシンプルなkv ❌ 不可
R2 画像・動画・ファイルなどオブジェクトストレージ ❌ 不可
Durable Objects リアルタイム状態管理(チャット、ゲーム) △ 限定的

構造化データで検索・フィルタ・JOINが必要なら迷わずD1を選びましょう。

セットアップ:D1データベースを作る

前提条件

  • Cloudflareアカウント(無料)
  • Node.js 18以上 + wrangler 3以上

1. D1データベースを作成する

# wranglerでD1データベースを作成
npx wrangler d1 create my-app-db

# 出力例:
# ✅ Successfully created DB 'my-app-db'
# [[d1_databases]]
# binding = "DB"
# database_name = "my-app-db"
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

出力された [[d1_databases]] ブロックをそのまま wrangler.toml にコピーします。

2. wrangler.tomlに追記

name = "my-app"
main = "src/index.ts"
compatibility_date = "2026-01-01"

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

3. スキーマを定義してマイグレーション

# migrations/0001_init.sql
CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  plan TEXT NOT NULL DEFAULT 'free',
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_items_user_id ON items(user_id);
# リモート(本番)に適用
npx wrangler d1 execute my-app-db --remote --file=migrations/0001_init.sql

# ローカル開発環境に適用
npx wrangler d1 execute my-app-db --local --file=migrations/0001_init.sql

広告・楽天アフィリエイト

開発者向けガジェット

PC周辺機器を楽天市場でチェック。

楽天ポイント還元

楽天市場を見る →

広告・楽天アフィリエイト

メカニカルキーボード

打鍵感で選ぶ開発者向けキーボード。

楽天ポイント還元

キーボードを楽天で見る →

Workers + D1:基本的なCRUD操作

TypeScriptでの基本的な使い方を見ていきましょう。

型定義

// src/types.ts
export interface Env {
  DB: D1Database;
}

export interface User {
  id: number;
  email: string;
  name: string;
  plan: string;
  created_at: string;
}

基本的なCRUD

// src/index.ts
import { Env, User } from './types';

export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);

    // ユーザー一覧取得(READ)
    if (url.pathname === '/users' && request.method === 'GET') {
      const { results } = await env.DB.prepare(
        'SELECT * FROM users ORDER BY created_at DESC LIMIT 20'
      ).all();
      return Response.json(results);
    }

    // ユーザー作成(CREATE)
    if (url.pathname === '/users' && request.method === 'POST') {
      const { email, name } = await request.json<{ email: string; name: string }>();
      const result = await env.DB.prepare(
        'INSERT INTO users (email, name) VALUES (?, ?) RETURNING *'
      ).bind(email, name).first();
      return Response.json(result, { status: 201 });
    }

    // ユーザー更新(UPDATE)
    if (url.pathname.startsWith('/users/') && request.method === 'PUT') {
      const id = url.pathname.split('/')[2];
      const { name } = await request.json<{ name: string }>();
      await env.DB.prepare(
        'UPDATE users SET name = ? WHERE id = ?'
      ).bind(name, id).run();
      return Response.json({ ok: true });
    }

    // ユーザー削除(DELETE)
    if (url.pathname.startsWith('/users/') && request.method === 'DELETE') {
      const id = url.pathname.split('/')[2];
      await env.DB.prepare('DELETE FROM users WHERE id = ?').bind(id).run();
      return Response.json({ ok: true });
    }

    return new Response('Not Found', { status: 404 });
  }
};

バッチ処理(複数クエリをまとめて実行)

// D1のbatch()でトランザクション的に複数クエリを実行
const [userResult, itemsResult] = await env.DB.batch([
  env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId),
  env.DB.prepare('SELECT * FROM items WHERE user_id = ? LIMIT 10').bind(userId),
]);

const user = userResult.results[0];
const items = itemsResult.results;
注意: D1の batch() はアトミックではありません(途中で失敗しても前のクエリはコミット済み)。本当のトランザクションが必要な場合は BEGIN / COMMIT を含むSQLを直接実行してください。

Honoと組み合わせる(推奨パターン)

Honoを使うと、ルーティング・バリデーション・ミドルウェアが簡潔に書けます。D1との相性も抜群です。

npm install hono @hono/zod-validator zod
// src/index.ts
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { Env } from './types';

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

// バリデーションスキーマ
const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

// ユーザー一覧
app.get('/api/users', async (c) => {
  const { results } = await c.env.DB.prepare(
    'SELECT id, email, name, plan, created_at FROM users ORDER BY created_at DESC'
  ).all();
  return c.json(results);
});

// ユーザー作成(バリデーション付き)
app.post('/api/users', zValidator('json', createUserSchema), async (c) => {
  const { email, name } = c.req.valid('json');
  try {
    const user = await c.env.DB.prepare(
      'INSERT INTO users (email, name) VALUES (?, ?) RETURNING *'
    ).bind(email, name).first();
    return c.json(user, 201);
  } catch (e) {
    if (e instanceof Error && e.message.includes('UNIQUE')) {
      return c.json({ error: 'Email already exists' }, 409);
    }
    return c.json({ error: 'Internal server error' }, 500);
  }
});

// ユーザーの投稿一覧(JOIN)
app.get('/api/users/:id/items', async (c) => {
  const userId = c.req.param('id');
  const { results } = await c.env.DB.prepare(`
    SELECT i
        .*, u.name as user_name
    FROM items i
    JOIN users u ON i.user_id = u.id
    WHERE i.user_id = ?
    ORDER BY i.created_at DESC
  `).bind(userId).all();
  return c.json(results);
});

export default app;

広告・楽天アフィリエイト

PCモニター

デュアルディスプレイや高解像度モデルをチェック。

楽天ポイント還元

モニターを楽天で見る →

広告・楽天アフィリエイト

プログラミング技術書

実践寄りの技術書・参考書を探す。

楽天ポイント還元

技術書を楽天で見る →

ローカル開発とデプロイ

ローカル開発(wrangler dev)

# ローカルでD1を使って開発(ファイルベースのSQLite)
npx wrangler dev --local

# ローカルDBに直接クエリを実行して確認
npx wrangler d1 execute my-app-db --local --command="SELECT * FROM users LIMIT 5;"

本番デプロイ

# Workersにデプロイ
npx wrangler deploy

# 本番DBの状態を確認
npx wrangler d1 execute my-app-db --remote --command="SELECT COUNT(*) as total FROM users;"

実践:マイクロSaaSへの応用パターン

D1を使った実際のSaaS構築例をご紹介します。当サイト関連プロダクトの StatusCraft(ステータスページ)PagePulse(Webページ監視) でも同様のパターンを採用しています。

プランベースのアクセス制御

// ユーザーのプランに応じてリソース制限を適用
app.post('/api/items', authMiddleware, async (c) => {
  const user = c.get('user');

  // プラン制限チェック
  const { results: [{ count }] } = await c.env.DB.prepare(
    'SELECT COUNT(*) as count FROM items WHERE user_id = ?'
  ).bind(user.id).all<{ count: number }>();

  const limit = user.plan === 'pro' ? 1000 : 3; // 無料は3件まで
  if (count >= limit) {
    return c.json({ error: 'Plan limit reached. Upgrade to Pro.' }, 403);
  }

  // アイテム作成
  const { title, content } = await c.req.json();
  const item = await c.env.DB.prepare(
    'INSERT INTO items (user_id, title, content) VALUES (?, ?, ?) RETURNING *'
  ).bind(user.id, title, content).first();
  return c.json(item, 201);
});

ページネーション

// カーソルベースのページネーション
app.get('/api/items', async (c) => {
  const cursor = c.req.query('cursor'); // 前ページ最後のID
  const limit = 20;

  const query = cursor
    ? 'SELECT * FROM items WHERE id < ? ORDER BY id DESC LIMIT ?'
    : 'SELECT * FROM items ORDER BY id DESC LIMIT ?';

  const { results } = cursor
    ? await c.env.DB.prepare(query).bind(cursor, limit).all()
    : await c.env.DB.prepare(query).bind(limit).all();

  const nextCursor = results.length === limit ? results[results.length - 1].id : null;
  return c.json({ items: results, nextCursor });
});

無料枠と料金の目安

2026年4月時点のD1料金は以下のとおりです(Workers無料プランに含まれる):

項目 無料枠 超過時の料金
読み取り 500万行/日 $0.001/100万行
書き込み 10万行/日 $1.00/100万行
ストレージ 5GB $0.75/GB/月
データベース数 500

月間アクティブユーザー数千人規模のSaaSでも、読み書きの量によっては無料枠内に収まることがほとんどです。まずは無料で始めて、成長に合わせてコストを払う構造が個人開発に最適です。

よくあるハマりポイントと対処法

1. SQLiteの型制約に注意

D1はSQLiteなので、厳密な型強制がありません。INTEGERカラムに文字列を入れてもエラーにならない場合があります。アプリ層でバリデーション(Zodなど)を必ずかけましょう。

2. 書き込みは単一プライマリノードに集中する

D1の書き込みはプライマリリージョンに集中します。書き込みが頻繁な場合は書き込みのレイテンシが高くなる可能性があります。読み取り重視のユースケースに向いています。

3. prepare().bind()でプレースホルダーを必ず使う

SQLインジェクション防止のため、ユーザー入力は必ず ? プレースホルダーと .bind() を使ってください。文字列連結でクエリを組み立てるのは厳禁です。

// ❌ 危険:文字列連結
env.DB.prepare(`SELECT * FROM users WHERE email = '${email}'`);

// ✅ 安全:プレースホルダー
env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(email);

広告・楽天アフィリエイト

ガジェット・オーディオ

イヤホン・便利ガジェットをポイント還元で。

楽天ポイント還元

ガジェットを楽天で見る →

まとめ

Cloudflare D1は個人開発者にとって理想的なデータベース選択肢です。

  • 無料枠が広い:小〜中規模SaaSなら$0運用が現実的
  • SQLite互換:普通のSQLがそのまま使える、学習コスト低
  • Workersとの統合:同一エコシステムで完結、デプロイが簡単
  • Honoとの相性が良い:型安全な高速APIが簡単に構築できる
  • ⚠️ 書き込みレイテンシ:書き込み頻度が非常に高い場合は要検討

Cloudflare Workers + D1 + Honoの組み合わせは、2026年現在の個人開発スタックとして非常に完成度が高く、月$0から始められる点で特に魅力的です。まだ試していない方は、ぜひこの機会にセットアップしてみてください。

広告・楽天アフィリエイト

USBハブ・周辺機器

ドッキング・ケーブル類をまとめて比較。

楽天ポイント還元

周辺機器を楽天で見る →

広告・楽天アフィリエイト

ノートパソコン特集

開発用ノートPCをスペック・口コミで比較。

楽天ポイント還元

ノートPCを楽天で見る →