Use Cases (更新: 2026/6/3)

Claude CodeでCloudflare Workersを実装する実践ガイド

Claude CodeでWorkers APIを設計し、Wrangler、KV/D1/R2、キャッシュ、ログまで実装する手順。

Claude CodeでCloudflare Workersを実装する実践ガイド

Cloudflare Workersは、Cloudflareのエッジネットワーク上でJavaScript/TypeScriptを動かす実行環境です。エッジとは「利用者に近い場所で処理する仕組み」のことで、東京の読者には東京近辺、欧州の読者には欧州近辺の拠点でリクエストを処理できます。

Claude Codeと相性が良い理由は、Workersの実装が「小さな入口関数」「明示的なbinding」「Wrangler設定」「curlで確認できるHTTP API」に分解しやすいからです。曖昧に「APIを作って」ではなく、src/index.tswrangler.toml、D1のSQL、KV/R2の役割、ログの観点まで渡すと、Claude Codeはレビューしやすい差分を作れます。

この記事では、2026年6月3日時点の公式ドキュメントを確認したうえで、Claude Codeへ何を頼み、何を人間が確認するべきかを実装順に整理します。公式情報は Cloudflare WorkersWranglerbindingsKVD1R2Cache APIRate LimitingWorkers Logs を必ず横に置いてください。サーバーレス全体の比較は Claude Codeサーバーレス関数ガイドAWS Lambda完全ガイド も参考になります。

まず作るもの

今回は「軽量な注文API」を題材にします。GETで注文を返し、POSTでD1に保存し、一覧レスポンスはCache APIで短時間キャッシュし、同じユーザーの連打はRate Limitingで止め、監査用のJSONログを残します。添付ファイルのメタデータはR2に保存できる形にしておきます。

flowchart LR
  Browser["Browser / API client"] --> Worker["Cloudflare Worker fetch handler"]
  Worker --> D1["D1: orders table"]
  Worker --> KV["KV: feature flags / small settings"]
  Worker --> R2["R2: receipts and exports"]
  Worker --> Cache["Cache API: GET response"]
  Worker --> Logs["Workers Logs"]

用語を平たく言うと、bindingは「Workerに外部リソースを渡す差し込み口」です。env.DBならD1、env.SETTINGSならKV、env.BUCKETならR2を指します。wrangler.tomlはその差し込み口を本番環境へ宣言する設定ファイルです。

Claude Codeに最初に渡す指示

最初のプロンプトは、成果物と制約をファイル単位で渡します。ここを雑にすると、Claude Codeは動くが運用できないサンプルを出しがちです。

Cloudflare Workers + TypeScriptで注文APIを実装してください。

変更対象:
- src/index.ts
- wrangler.toml
- schema.sql

要件:
- fetch handler形式で実装する
- GET /health はJSONを返す
- GET /orders/:id はD1から1件取得し、公開可能なレスポンスだけ30秒キャッシュする
- POST /orders はJSONを検証してD1へ保存する
- Authorization Bearerトークンを環境変数API_TOKENで検証する
- KV binding SETTINGS、D1 binding DB、R2 binding RECEIPTS、Rate Limit binding API_RATE_LIMITERを使う
- security headersを全レスポンスへ付ける
- console.logはJSONオブジェクトで出す
- npm testなしでもcurlで検証できるコマンドをREADMEに書ける粒度にする

禁止:
- secret値をwrangler.tomlへ直書きしない
- Node.js専用APIを前提にしない
- 疑似コードで済ませない

「fetch handler形式」は、Workerの入口を export default { async fetch(request, env, ctx) { ... } } として書く形です。Expressのように常駐サーバーをlistenするのではなく、リクエストごとに関数が呼ばれると考えると理解しやすいです。

プロジェクト作成とWrangler設定

WranglerはCloudflare Developer PlatformのCLIです。ローカル実行、D1/KV/R2の作成、secret登録、deploy、ログ確認を担当します。C3の新規プロジェクトではwrangler.jsoncが生成される場合がありますが、WranglerはJSON/JSONCとTOMLの設定ファイルに対応しています。この記事では読みやすさのためにwrangler.tomlで示します。

npm create cloudflare@latest claude-worker-api -- --type=hello-world
cd claude-worker-api
npm install -D typescript wrangler
npx wrangler --version

wrangler.tomlは次の形から始めます。database_id、KVのid、R2のbucket_nameは自分の環境で作成した値に置き換えます。varsは公開されても困らない設定値だけに使い、APIトークンなどのsecretは後述の wrangler secret put で登録します。

name = "claude-worker-api"
main = "src/index.ts"
compatibility_date = "2026-06-03"

[vars]
PUBLIC_ENV = "production"

[observability]
enabled = true
head_sampling_rate = 1

[[d1_databases]]
binding = "DB"
database_name = "claude-worker-api"
database_id = "replace-with-d1-database-id"

[[kv_namespaces]]
binding = "SETTINGS"
id = "replace-with-kv-namespace-id"

[[r2_buckets]]
binding = "RECEIPTS"
bucket_name = "claude-worker-receipts"

[[ratelimits]]
name = "API_RATE_LIMITER"
namespace_id = "1001"

  [ratelimits.simple]
  limit = 60
  period = 60

関連リソースはCLIで作れます。

npx wrangler login
npx wrangler d1 create claude-worker-api
npx wrangler kv namespace create SETTINGS
npx wrangler r2 bucket create claude-worker-receipts
npx wrangler secret put API_TOKEN

Rate Limiting bindingはWrangler 4.36.0以降が必要です。ローカル開発で未対応のbindingもあるため、Rate Limitingそのものの最終確認は本番またはリモート環境で行う前提にしてください。

D1スキーマ

D1はSQLite互換のサーバーレスデータベースです。小さなAPI、管理画面、Webhookの受信履歴などには扱いやすい一方、複雑なOLTPや重い集計を全部任せる設計には向きません。

-- schema.sql
CREATE TABLE IF NOT EXISTS orders (
  id TEXT PRIMARY KEY,
  email TEXT NOT NULL,
  amount INTEGER NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_orders_email ON orders(email);
npx wrangler d1 execute claude-worker-api --local --file=./schema.sql
npx wrangler d1 execute claude-worker-api --remote --file=./schema.sql

--localは開発用、--remoteはCloudflare側のD1に対する実行です。Claude Codeにマイグレーションを書かせたら、必ず「既存テーブルに対して破壊的でないか」を人間が確認します。

動くsrc/index.ts

次のコードは、依存ライブラリなしで動く最小APIです。Honoを使う選択肢もありますが、まずはWorkersの素の形を理解したほうが、Claude Codeの差分レビューが楽になります。

// src/index.ts
export interface Env {
  API_TOKEN: string;
  PUBLIC_ENV: string;
  DB: D1Database;
  SETTINGS: KVNamespace;
  RECEIPTS: R2Bucket;
  API_RATE_LIMITER: RateLimit;
}

type OrderInput = {
  email: string;
  amount: number;
};

const securityHeaders = {
  "content-security-policy": "default-src 'none'; frame-ancestors 'none'",
  "x-content-type-options": "nosniff",
  "x-frame-options": "DENY",
  "referrer-policy": "no-referrer",
  "permissions-policy": "camera=(), microphone=(), geolocation=()",
};

function json(data: unknown, init: ResponseInit = {}) {
  return new Response(JSON.stringify(data), {
    ...init,
    headers: {
      "content-type": "application/json; charset=utf-8",
      ...securityHeaders,
      ...init.headers,
    },
  });
}

function requireAuth(request: Request, env: Env) {
  const expected = `Bearer ${env.API_TOKEN}`;
  return request.headers.get("authorization") === expected;
}

async function readOrderInput(request: Request): Promise<OrderInput> {
  const body = await request.json<OrderInput>();

  if (!body.email || !body.email.includes("@")) {
    throw new Error("email must be valid");
  }

  if (!Number.isInteger(body.amount) || body.amount <= 0) {
    throw new Error("amount must be a positive integer");
  }

  return body;
}

export default {
  async fetch(request, env, ctx): Promise<Response> {
    const url = new URL(request.url);
    const requestId = crypto.randomUUID();

    console.log({
      event: "request_started",
      requestId,
      method: request.method,
      path: url.pathname,
    });

    if (url.pathname === "/health") {
      const maintenance = await env.SETTINGS.get("maintenance");
      return json({ ok: true, env: env.PUBLIC_ENV, maintenance: maintenance === "true" });
    }

    if (!requireAuth(request, env)) {
      return json({ error: "unauthorized" }, { status: 401 });
    }

    const rateKey = request.headers.get("authorization")?.slice(-16) ?? "anonymous";
    const { success } = await env.API_RATE_LIMITER.limit({ key: rateKey });

    if (!success) {
      return json({ error: "rate_limited" }, { status: 429 });
    }

    const orderMatch = url.pathname.match(/^\/orders\/([a-zA-Z0-9_-]+)$/);

    if (request.method === "GET" && orderMatch) {
      const cache = caches.default;
      const cacheKey = new Request(url.toString(), { method: "GET" });
      const cached = await cache.match(cacheKey);

      if (cached) {
        return cached;
      }

      const order = await env.DB.prepare(
        "SELECT id, email, amount, status, created_at FROM orders WHERE id = ?"
      ).bind(orderMatch[1]).first();

      if (!order) {
        return json({ error: "not_found" }, { status: 404 });
      }

      const response = json({ order }, {
        headers: {
          "cache-control": "public, max-age=30",
          "cache-tag": `order-${orderMatch[1]}`,
        },
      });

      ctx.waitUntil(cache.put(cacheKey, response.clone()));
      return response;
    }

    if (request.method === "POST" && url.pathname === "/orders") {
      try {
        const input = await readOrderInput(request);
        const id = crypto.randomUUID();

        await env.DB.prepare(
          "INSERT INTO orders (id, email, amount, status) VALUES (?, ?, ?, ?)"
        ).bind(id, input.email, input.amount, "pending").run();

        await env.RECEIPTS.put(`orders/${id}.json`, JSON.stringify({
          id,
          email: input.email,
          amount: input.amount,
        }), {
          httpMetadata: { contentType: "application/json" },
        });

        console.log({ event: "order_created", requestId, orderId: id });
        return json({ id, status: "pending" }, { status: 201 });
      } catch (error) {
        return json({ error: error instanceof Error ? error.message : "bad_request" }, { status: 400 });
      }
    }

    return json({ error: "not_found" }, { status: 404 });
  },
} satisfies ExportedHandler<Env>;

このサンプルで重要なのは、secretをコードに埋め込まないこと、ctx.waitUntilでキャッシュ保存をレスポンス後に回すこと、console.logを文字列ではなくJSONオブジェクトで残すことです。Workers LogsはJSONフィールドを追いやすいため、後から requestIdorderId で絞り込みやすくなります。

ローカル確認とデプロイ確認

ローカルでは .dev.vars にだけ開発用secretを置けます。本番secretとは分けます。

printf "API_TOKEN=dev-token\n" > .dev.vars
npx wrangler dev

別ターミナルで確認します。

curl http://localhost:8787/health

curl -X POST http://localhost:8787/orders \
  -H "authorization: Bearer dev-token" \
  -H "content-type: application/json" \
  -d "{\"email\":\"masa@example.com\",\"amount\":1200}"

curl http://localhost:8787/orders/replace-with-created-id \
  -H "authorization: Bearer dev-token"

本番へ出す前に、secret登録、D1のremote migration、ログ設定を確認します。

npx wrangler secret put API_TOKEN
npx wrangler d1 execute claude-worker-api --remote --file=./schema.sql
npx wrangler deploy
npx wrangler tail

wrangler tailはリアルタイム確認に便利ですが、継続的な調査にはWorkers Logsの画面またはLogpushを使います。公開後は curl -i でセキュリティヘッダー、cache-control、429レスポンスを確認してください。

3つ以上の実用ユースケース

1つ目は、問い合わせフォームや小さな注文APIです。D1に本文と状態を保存し、R2に添付ファイルや受領JSONを置き、管理画面から状態更新する構成にできます。利用者に近い場所で受け付けるので、海外読者が多い個人サービスにも向きます。

2つ目は、Webhookの受け口です。GitHub、Stripe、GumroadなどからPOSTを受け、署名検証をしてD1へイベントIDを保存します。同じイベントIDが来たら処理しない冪等性を持たせると、再送に強くなります。詳しい署名検証の考え方は Claude Code Webhook実装 と合わせて読むと整理しやすいです。

3つ目は、軽量なBFFです。BFFはBackend for Frontend、つまり「フロントエンド専用の薄いAPI層」です。ブラウザから直接見せたくないAPIキーをWorker側で扱い、外部APIのレスポンスを整形し、Cache APIで短時間キャッシュします。個人開発のダッシュボードや多言語ブログのメタデータAPIに向いています。

4つ目を挙げるなら、画像・PDF・CSVなどの配布制御です。R2にファイルを置き、Workerで認可、署名URL、ダウンロードログを扱います。ただし大きな変換処理や長時間ジョブはWorkers単体よりCloud Run、Lambda、Queuesなどを検討します。

落とし穴

最初の落とし穴は、Node.jsサーバーのつもりで書くことです。WorkersはWeb標準API中心のランタイムです。Node.js互換は広がっていますが、fsでローカルファイルを読む、Expressをlistenする、プロセス常駐メモリを信頼する設計は避けます。

2つ目は、KV、D1、R2、Cache APIの役割を混ぜることです。KVは小さな設定や参照頻度の高いキー値、D1は関係データ、R2はオブジェクト、Cache APIは短寿命レスポンスの高速化に使います。何でもKVに入れると、検索や整合性で困ります。

3つ目は、キャッシュに個人情報を入れることです。Set-Cookie付きのレスポンス、認可ユーザーごとに違うレスポンス、メールアドレスを含むレスポンスは、原則として共有キャッシュに入れません。キャッシュキーにユーザーIDを含める場合でも、漏えい時の影響を考えて短いTTLにします。

4つ目は、Rate LimitingのキーをIPだけにすることです。モバイル回線や会社ネットワークでは多数の利用者が同じIPに見えることがあります。公式ドキュメントでも、ユーザーID、APIキー、テナントIDなど安定した識別子が推奨されています。

5つ目は、ログへsecretや個人情報を出すことです。authorizationヘッダー、Cookie、メール本文、APIトークンは出してはいけません。ログは便利ですが、後から検索できる形で残るため、出す情報を最小限にします。

Workers、Pages Functions、Cloud Run、Lambdaの選び方

Workersを選ぶのは、HTTP APIを低遅延で返したい、Cloudflareのcacheやbindingsを直接使いたい、グローバル配信に近い場所で軽い処理をしたい場合です。今回のような小さなAPI、Webhook、BFFにはよく合います。

Pages Functionsを選ぶのは、Cloudflare Pagesで静的サイトやフロントエンドを配信していて、その横に少量のサーバー処理を足したい場合です。フォーム送信、認証の補助、ページ単位のAPIには自然です。単独のAPI基盤として育つならWorkersのほうが設定と責務を分けやすいです。

Cloud Runを選ぶのは、コンテナで任意の言語・フレームワークを動かしたい、画像変換やPDF生成など処理時間が長い、既存のExpress/FastAPI/Railsを持ち込みたい場合です。Googleの公式ドキュメントでもCloud Runはサービス、ジョブ、worker poolとしてコードを動かせる管理基盤として説明されています。

Lambdaを選ぶのは、AWS内のS3、DynamoDB、EventBridge、SQSなどと近いイベント処理を組む場合です。AWSの権限設計やVPC接続が必要ならLambdaが自然です。Cloudflareのエッジキャッシュに近いAPIならWorkers、AWSイベント中心ならLambda、と境界を決めると迷いにくくなります。

Claude Codeレビュー用プロンプト

実装後は、生成ではなくレビューを頼みます。

このCloudflare Workers実装をレビューしてください。

観点:
- fetch handlerがWorkersランタイムで動くか
- wrangler.tomlのbinding名とEnv型が一致しているか
- API_TOKENなどのsecretがコード、ログ、設定ファイルへ漏れていないか
- D1クエリがbindを使い、SQL injectionに弱くないか
- Cache APIに個人情報や認可レスポンスを入れていないか
- Rate LimitingのkeyがIP依存だけになっていないか
- security headersが全レスポンスへ付くか
- curlで再現できる検証手順が足りているか

出力は、重大度順の指摘、修正案、追加すべきテストだけにしてください。

このプロンプトは、Claude Codeを「実装者」から「厳しいレビュアー」に切り替えるためのものです。特にbinding名のズレ、secretの扱い、キャッシュの誤用は、人間が目視しても見落としやすいので機械レビューを挟む価値があります。

CTA

既存のAPIをWorkersへ寄せるなら、まず1エンドポイントだけ移植してください。/health、読み取り専用GET、Webhook受信のどれかが安全です。Claude Codeには「ファイル単位の変更対象」「binding名」「curl検証」「やってはいけないこと」を先に渡すと、レビュー可能な差分になります。実務用のテンプレートやチェックリストは プロダクト一覧 を、チームでClaude Code運用を固めたい場合は 研修プログラム を確認してください。セキュリティヘッダーの考え方は Claude Code Webセキュリティヘッダー実装 も併読してください。

実際に試した結果

実際に試した結果、この記事のコードはWranglerの設定、D1スキーマ、src/index.tsの構文、curlでの確認手順を一通りつなげて確認できました。実運用ではRate Limiting bindingやWorkers Logsの保持期間、D1/R2の課金条件がアカウント設定に依存するため、公開前に自分のCloudflareアカウントで wrangler deploywrangler tail、429レスポンス、キャッシュヘッダーを再確認してください。

#Claude Code #Cloudflare Workers #edge computing #serverless #API
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。