cloudflareworkersd1wranglerserverless

Cloudflare Workers と D1 でサーバーレス注文 API を構築する

TPThuanPD2026年9月10日60分新着
Cloudflare

AWS ラボの注文 API を Cloudflare で作り直します。1 つの Worker が POST と GET を処理し、データは D1 に保存します。作成から deploy まで Wrangler CLI で行い、Lambda、DynamoDB、API Gateway と直接比較します。

アーキテクチャ図: USERS → HTTPS *.workers.dev → Worker order-api(POST /orders → createOrder、GET /orders → getOrder)→ binding DB → D1 orders-db の orders テーブル。注記: Worker ≈ API Gateway + 2 つの Lambda、binding ≈ IAM ロール、D1 ≈ DynamoDB

Lab Overview and Learning Objectives

開始前の注意

本ラボは、AWS ラボ「Lambda、DynamoDB、API Gateway でサーバーレス CRUD API を構築する」とまったく同じ課題を、カバーの図のとおり Cloudflare 上で解きます。ユーザーは HTTPS で 1 つの Worker を呼び出し、Worker 自身が POST(注文作成)と GET(注文参照)を振り分け、データは binding DB を通じて D1 データベースで読み書きされます。すべて Wrangler CLI で操作し、deploy 前にローカルで動作確認します。

  1. Cloudflare アカウントを準備します。Workers と D1 には無料枠があるため、Free プランで十分です。
  2. Node.js 22 以降をインストールし(最新の Wrangler は Node 22 以上が必要)、node --version で確認します。
  3. ターミナルと API クライアントを用意します: curl(macOS、Linux、Windows 10 以降に標準搭載)または Postman。
  4. サンドボックスアカウントを使い、サンプルの注文に実データや個人情報を入れないでください。
目的:ラボ完了後、https://order-api.<subdomain>.workers.dev で POST /orders と GET /orders?orderId=... を持つ公開 REST API が動作します。AWS 版と同じ API 仕様・同じ JSON body で、Worker 上で動き、データは D1 に保存されます。
1

1. AWS 版とのアーキテクチャ対応

両者は同じ endpoint と同じデータを扱いますが、構築する部品の数が異なります。以降の各手順が AWS 版のどの手順に当たるのか、先にこの対応表で確認します。

  1. API Gateway + CreateOrderFunction + GetOrderFunction → 1 つの Worker order-api。fetch handler が method と path で振り分けます。
  2. IAM ロール LambdaOrderRole → wrangler.jsonc の binding DB。Worker は bind されたデータベースにだけアクセスでき、ロールや policy を作る必要はありません。
  3. partition key が orderId の DynamoDB Orders テーブル → 主キーが order_id の D1 orders テーブル。SQL でクエリします。
  4. dev stage への deploy と Invoke URL → wrangler deploy。すぐに *.workers.dev の URL が得られます。
  5. CloudWatch Logs → wrangler tail(リアルタイムログ)と Dashboard の Workers Logs。
比較用に AWS 版ラボを開く: Lambda、DynamoDB、API Gateway でサーバーレス CRUD API
完了条件:カバー図の ≈ 注記のとおり、どの Cloudflare の部品がどの AWS の部品に代わるかを把握できます。
2

2. C3 で Worker プロジェクトを作成

create-cloudflare(C3)は wrangler.jsonc と src/index.ts を含む TypeScript プロジェクトのひな形を作り、Wrangler をプロジェクト内にインストールします。--no-deploy フラグにより、deploy の手順まで Worker はローカルにとどまります。

  1. 作業フォルダーでターミナルを開き、下記のコマンドを実行します。
  2. これらのフラグは、手動で Hello World example → Worker only → TypeScript を選ぶのと同じです。git や AGENTS.md について聞かれた場合は任意で答えて構いません。ラボには影響しません。
  3. C3 が完了したら、プロジェクトへ移動します: cd order-api。
Terminal
npm create cloudflare@latest -- order-api --type=hello-world --lang=ts --no-deploy
注意:依存関係のインストールで Cannot read properties of null (reading 'edgesOut') が出た場合(テンプレートの vitest の peer dependency を解決する際の npm 10 の不具合)、cd order-api の後に npm install --legacy-peer-deps を実行します。この場合 wrangler.jsonc には <WORKER_NAME> のプレースホルダーが残るため、手順 4 で完成版のファイルに置き換えます。
完了条件:order-api フォルダーに wrangler.jsonc、src/index.ts、そして dev、deploy、cf-typegen スクリプトを持つ package.json ができます。
3

3. Wrangler にログイン

データベースの作成と deploy は Cloudflare アカウントを直接操作するため、Wrangler にブラウザー経由(OAuth)で一度だけ権限を与える必要があります。AWS の認証情報の設定に相当しますが、アクセスキーの作成は不要です。

  1. npx wrangler login を実行します。ブラウザーで認可ページが開いたら Allow を押します。
  2. ターミナルに戻り、npx wrangler whoami を実行します。
  3. メールアドレスが複数のアカウントに属している場合は、ラボで使うアカウントを控えます。
Terminal
npx wrangler login
npx wrangler whoami
完了条件:wrangler whoami にメールアドレスと Account ID が表示されます。
4

4. D1 データベースを作成して binding を宣言

D1 は SQLite をベースにした Cloudflare のサーバーレス SQL データベースです。binding は Worker にデータベースを使わせる仕組みで、wrangler.jsonc に一度宣言すればコードからは env.DB として参照でき、接続文字列や IAM policy は不要です。

  1. npx wrangler d1 create orders-db --binding DB --update-config を実行します。--location apac を付けると、データベースをアジア太平洋に置くようヒントを与えられます。
  2. Wrangler がデータベースを作成し、database_id を表示して、wrangler.jsonc に d1_databases ブロックを追加します。
  3. wrangler.jsonc を開き、下記のサンプルと照合します: name は order-api、binding は DB、database_id は作成した ID です。<WORKER_NAME> や <COMPATIBILITY_DATE> のプレースホルダーが残っている場合は、サンプルの値に置き換えます。
  4. npm run cf-typegen を実行して worker-configuration.d.ts を再生成します。Env 型に DB: D1Database が追加されます。@types/node のインストールを勧められても、本ラボでは省略して構いません。
wrangler.jsonc
{
  "name": "order-api",
  "main": "src/index.ts",
  "compatibility_date": "2026-09-01",
  "observability": {
    "enabled": true
  },
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "orders-db",
      "database_id": "<DATABASE_ID>"
    }
  ]
}
完了条件:wrangler.jsonc に orders-db を指す binding DB があり、TypeScript が env.DB を認識します。
5

5. SQL で orders テーブルを作成

DynamoDB は partition key の宣言だけで済みますが、D1 は列、型、制約を持つ SQL スキーマを使います。order_id の PRIMARY KEY が partition key orderId の役割を果たし、CHECK によって負の価格をデータベース側で拒否します。

  1. プロジェクトのルートに schema.sql を作成し、下記の内容を記述します。
  2. wrangler dev で使うローカルデータベースにスキーマを適用します: npx wrangler d1 execute orders-db --local --file=./schema.sql
  3. Cloudflare 上のデータベースにスキーマを適用します: npx wrangler d1 execute orders-db --remote --file=./schema.sql。確認を求められたら Yes を選びます。
  4. テーブルを確認します: npx wrangler d1 execute orders-db --remote --command="SELECT name FROM sqlite_master WHERE type='table'"
schema.sql · SQL
-- Orders table: the D1 counterpart of the DynamoDB table keyed by orderId.
CREATE TABLE IF NOT EXISTS orders (
  order_id   TEXT PRIMARY KEY,
  item       TEXT NOT NULL,
  price      REAL NOT NULL CHECK (price >= 0),
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
注意:ローカルとリモートは別々のデータベースです。wrangler dev で作成したデータは Cloudflare 上に現れず、その逆も同様なので、スキーマは両方に適用する必要があります。
完了条件:ローカルとリモートの両方のデータベースに、_cf_ で始まるシステムテーブルとともに orders テーブルができます。
6

6. POST と GET を処理する Worker を記述

2 つの Lambda 関数のロジックが 1 つのファイルに収まります。fetch handler が API Gateway の役割を担って method で振り分け、createOrder と getOrder がそれぞれ CreateOrderFunction と GetOrderFunction に対応します。SQL は bind() 付きの prepared statement で SQL インジェクションを防ぎ、D1 は JavaScript の数値を返すため、Python 版の DecimalEncoder は不要です。

  1. src/index.ts を開き、Hello World のコードを削除して下記のコードをすべて貼り付けます。
  2. POST は body を検証します: orderId と item は空でない文字列、price は 0 以上の数値でなければならず、違反時は 400 を返します。
  3. INSERT ... ON CONFLICT DO NOTHING: orderId が既に存在する場合は行が変更されず(meta.changes = 0)、DynamoDB の put_item のように黙って上書きする代わりに 409 を返します。
  4. GET は注文が存在しない場合、Lambda 版のように {} を返すのではなく 404 を返します。
  5. 任意: npx tsc --noEmit で型チェックします。
src/index.ts · TypeScript
// Order API: POST /orders creates an order, GET /orders?orderId=... reads one.
// env.DB is the D1 database bound in wrangler.jsonc.
export default {
  async fetch(request, env): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname !== "/orders") {
      return Response.json({ error: "Not found" }, { status: 404 });
    }

    switch (request.method) {
      case "POST":
        return createOrder(request, env);
      case "GET":
        return getOrder(url, env);
      default:
        return Response.json(
          { error: "Method not allowed" },
          { status: 405, headers: { Allow: "GET, POST" } },
        );
    }
  },
} satisfies ExportedHandler<Env>;

async function createOrder(request: Request, env: Env): Promise<Response> {
  const body = await request.json<Record<string, unknown>>().catch(() => null);
  if (!body || typeof body !== "object") {
    return Response.json({ error: "Body must be a JSON object" }, { status: 400 });
  }

  const { orderId, item, price } = body;
  if (
    typeof orderId !== "string" || !orderId ||
    typeof item !== "string" || !item ||
    typeof price !== "number" || price < 0
  ) {
    return Response.json(
      { error: "orderId and item must be non-empty strings, price a number >= 0" },
      { status: 400 },
    );
  }

  // An existing orderId leaves the row untouched, so no row changes.
  const { meta } = await env.DB.prepare(
    "INSERT INTO orders (order_id, item, price) VALUES (?, ?, ?) ON CONFLICT (order_id) DO NOTHING",
  )
    .bind(orderId, item, price)
    .run();

  if (meta.changes === 0) {
    return Response.json({ error: `Order ${orderId} already exists` }, { status: 409 });
  }
  return Response.json({ message: "Order created successfully!", orderId }, { status: 201 });
}

async function getOrder(url: URL, env: Env): Promise<Response> {
  const orderId = url.searchParams.get("orderId");
  if (!orderId) {
    return Response.json({ error: "Missing orderId query parameter" }, { status: 400 });
  }

  const order = await env.DB.prepare(
    "SELECT order_id AS orderId, item, price, created_at AS createdAt FROM orders WHERE order_id = ?",
  )
    .bind(orderId)
    .first();

  if (!order) {
    return Response.json({ error: `Order ${orderId} not found` }, { status: 404 });
  }
  return Response.json(order);
}
完了条件:src/index.ts に POST /orders と GET /orders を処理する完成版の Worker があり、型チェックでエラーが出ません。
7

7. wrangler dev でローカルテスト

wrangler dev は、本番と同じ workerd ランタイムで Worker を手元のマシン上で動かし、手順 5 でスキーマを適用したローカル D1 も使います。deploy 不要で素早く試せ、アカウントの無料枠も消費しません。Lambda は deploy しないと API Gateway から呼び出せないため、AWS 版ラボにはこれに相当する手順がありません。

  1. プロジェクトフォルダーに order.json を作成し、{ "orderId": "123", "item": "Laptop", "price": 1500 } を記述します。
  2. npx wrangler dev を実行します。Worker は http://localhost:8787 で待ち受けます。
  3. 同じフォルダーで 2 つ目のターミナルを開き、下記の 2 つのコマンドを実行します: POST は "Order created successfully!" とともに 201 を返し、GET は createdAt を含む注文 123 の JSON を返します。
  4. POST をもう一度実行します: orderId が既に存在するため、今度は 409 になります。GET を orderId=999 に変えると 404 になります。
  5. Windows PowerShell 5.1 では curl が Invoke-WebRequest のエイリアスのため、curl の代わりに curl.exe と入力します。
  6. 1 つ目のターミナルで x または Ctrl+C を押して wrangler dev を停止します。
Terminal · curl
curl -i -X POST http://localhost:8787/orders -H "Content-Type: application/json" --data "@order.json"
curl -i "http://localhost:8787/orders?orderId=123"
完了条件:ローカル API が、作成で 201、参照で 200、orderId 重複で 409、注文なしで 404 を返します。
8

8. Cloudflare に deploy

wrangler deploy は src/index.ts をバンドルし、Cloudflare のネットワークに公開します。API Gateway のような stage の概念はなく、deploy ごとに新しいバージョンが作られ、workers.dev の URL がすぐにそのバージョンを配信します。

  1. npx wrangler deploy を実行します。
  2. アカウントに workers.dev のサブドメインがまだない場合は、Wrangler が登録を求めます。名前を決めて確定します。
  3. 表示された URL(https://order-api.<subdomain>.workers.dev の形)を控えます。
Terminal
npx wrangler deploy
完了条件:Worker order-api が Cloudflare 上で動作し、binding DB で本物の orders-db データベースを使います。
9

9. workers.dev で API をテスト

手順 7 のテストを本番の URL に対して繰り返します。今度は request が Cloudflare のネットワークを通り、リモートのデータベースに書き込まれます。並行して wrangler tail を開き、CloudWatch Logs に相当する request ごとのログを確認します。

  1. 1 つ目のターミナルで npx wrangler tail を実行し、ログをリアルタイムで表示します。
  2. 2 つ目のターミナルで、<subdomain> を自分のサブドメインに置き換えてから下記の 2 つのコマンドを実行します。
  3. POST は 201、GET は注文 123 の JSON を返し、各 request が wrangler tail にログとして表示されます。
  4. Ctrl+C で wrangler tail を停止します。
Terminal · curl
curl -i -X POST https://order-api.<subdomain>.workers.dev/orders -H "Content-Type: application/json" --data "@order.json"
curl -i "https://order-api.<subdomain>.workers.dev/orders?orderId=123"
完了条件:公開 API がローカル実行時と同じ結果を返し、request のログが wrangler tail に表示されます。
10

10. D1 でデータを確認

DynamoDB の Explore items と同様に、ターミナルまたは Dashboard から SQL で D1 を直接照会できます。

  1. 下記のコマンドを実行し、リモートデータベースの全注文を表示します。
  2. または Cloudflare Dashboard で D1 を開き、orders-db データベースを選択して、データベースのコンソールで同じ SQL を実行します。
  3. 注文 123 の item、price、createdAt が正しいことを確認します。
Terminal
npx wrangler d1 execute orders-db --remote --command="SELECT * FROM orders"
完了条件:orders テーブルに API 経由で作成した注文があり、Worker → binding DB → D1 の流れが端から端まで動作していることを確認できます。
11

11. クリーンアップ

本ラボの Worker とデータベースは無料枠の範囲に収まりますが、アカウントを整理するため、終了後は削除します。

  1. Worker を削除します: プロジェクトフォルダーで npx wrangler delete を実行し、確認します。
  2. データベースを削除します: npx wrangler d1 delete orders-db を実行し、確認します。
  3. このマシンで Wrangler を使わなくなる場合は、npx wrangler logout を実行します。
Terminal
npx wrangler delete
npx wrangler d1 delete orders-db
完了条件:Worker order-api とデータベース orders-db がアカウントに残っていない状態です。

参考ドキュメント