Cloudflare Workers と D1 でサーバーレス注文 API を構築する
AWS ラボの注文 API を Cloudflare で作り直します。1 つの Worker が POST と GET を処理し、データは D1 に保存します。作成から deploy まで Wrangler CLI で行い、Lambda、DynamoDB、API Gateway と直接比較します。
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 前にローカルで動作確認します。
- Cloudflare アカウントを準備します。Workers と D1 には無料枠があるため、Free プランで十分です。
- Node.js 22 以降をインストールし(最新の Wrangler は Node 22 以上が必要)、node --version で確認します。
- ターミナルと API クライアントを用意します: curl(macOS、Linux、Windows 10 以降に標準搭載)または Postman。
- サンドボックスアカウントを使い、サンプルの注文に実データや個人情報を入れないでください。
1. AWS 版とのアーキテクチャ対応
両者は同じ endpoint と同じデータを扱いますが、構築する部品の数が異なります。以降の各手順が AWS 版のどの手順に当たるのか、先にこの対応表で確認します。
- API Gateway + CreateOrderFunction + GetOrderFunction → 1 つの Worker order-api。fetch handler が method と path で振り分けます。
- IAM ロール LambdaOrderRole → wrangler.jsonc の binding DB。Worker は bind されたデータベースにだけアクセスでき、ロールや policy を作る必要はありません。
- partition key が orderId の DynamoDB Orders テーブル → 主キーが order_id の D1 orders テーブル。SQL でクエリします。
- dev stage への deploy と Invoke URL → wrangler deploy。すぐに *.workers.dev の URL が得られます。
- CloudWatch Logs → wrangler tail(リアルタイムログ)と Dashboard の Workers Logs。
2. C3 で Worker プロジェクトを作成
create-cloudflare(C3)は wrangler.jsonc と src/index.ts を含む TypeScript プロジェクトのひな形を作り、Wrangler をプロジェクト内にインストールします。--no-deploy フラグにより、deploy の手順まで Worker はローカルにとどまります。
- 作業フォルダーでターミナルを開き、下記のコマンドを実行します。
- これらのフラグは、手動で Hello World example → Worker only → TypeScript を選ぶのと同じです。git や AGENTS.md について聞かれた場合は任意で答えて構いません。ラボには影響しません。
- C3 が完了したら、プロジェクトへ移動します: cd order-api。
npm create cloudflare@latest -- order-api --type=hello-world --lang=ts --no-deploy3. Wrangler にログイン
データベースの作成と deploy は Cloudflare アカウントを直接操作するため、Wrangler にブラウザー経由(OAuth)で一度だけ権限を与える必要があります。AWS の認証情報の設定に相当しますが、アクセスキーの作成は不要です。
- npx wrangler login を実行します。ブラウザーで認可ページが開いたら Allow を押します。
- ターミナルに戻り、npx wrangler whoami を実行します。
- メールアドレスが複数のアカウントに属している場合は、ラボで使うアカウントを控えます。
npx wrangler login
npx wrangler whoami4. D1 データベースを作成して binding を宣言
D1 は SQLite をベースにした Cloudflare のサーバーレス SQL データベースです。binding は Worker にデータベースを使わせる仕組みで、wrangler.jsonc に一度宣言すればコードからは env.DB として参照でき、接続文字列や IAM policy は不要です。
- npx wrangler d1 create orders-db --binding DB --update-config を実行します。--location apac を付けると、データベースをアジア太平洋に置くようヒントを与えられます。
- Wrangler がデータベースを作成し、database_id を表示して、wrangler.jsonc に d1_databases ブロックを追加します。
- wrangler.jsonc を開き、下記のサンプルと照合します: name は order-api、binding は DB、database_id は作成した ID です。<WORKER_NAME> や <COMPATIBILITY_DATE> のプレースホルダーが残っている場合は、サンプルの値に置き換えます。
- npm run cf-typegen を実行して worker-configuration.d.ts を再生成します。Env 型に DB: D1Database が追加されます。@types/node のインストールを勧められても、本ラボでは省略して構いません。
{
"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>"
}
]
}5. SQL で orders テーブルを作成
DynamoDB は partition key の宣言だけで済みますが、D1 は列、型、制約を持つ SQL スキーマを使います。order_id の PRIMARY KEY が partition key orderId の役割を果たし、CHECK によって負の価格をデータベース側で拒否します。
- プロジェクトのルートに schema.sql を作成し、下記の内容を記述します。
- wrangler dev で使うローカルデータベースにスキーマを適用します: npx wrangler d1 execute orders-db --local --file=./schema.sql
- Cloudflare 上のデータベースにスキーマを適用します: npx wrangler d1 execute orders-db --remote --file=./schema.sql。確認を求められたら Yes を選びます。
- テーブルを確認します: npx wrangler d1 execute orders-db --remote --command="SELECT name FROM sqlite_master WHERE type='table'"
-- 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
);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 は不要です。
- src/index.ts を開き、Hello World のコードを削除して下記のコードをすべて貼り付けます。
- POST は body を検証します: orderId と item は空でない文字列、price は 0 以上の数値でなければならず、違反時は 400 を返します。
- INSERT ... ON CONFLICT DO NOTHING: orderId が既に存在する場合は行が変更されず(meta.changes = 0)、DynamoDB の put_item のように黙って上書きする代わりに 409 を返します。
- GET は注文が存在しない場合、Lambda 版のように {} を返すのではなく 404 を返します。
- 任意: npx tsc --noEmit で型チェックします。
// 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);
}7. wrangler dev でローカルテスト
wrangler dev は、本番と同じ workerd ランタイムで Worker を手元のマシン上で動かし、手順 5 でスキーマを適用したローカル D1 も使います。deploy 不要で素早く試せ、アカウントの無料枠も消費しません。Lambda は deploy しないと API Gateway から呼び出せないため、AWS 版ラボにはこれに相当する手順がありません。
- プロジェクトフォルダーに order.json を作成し、{ "orderId": "123", "item": "Laptop", "price": 1500 } を記述します。
- npx wrangler dev を実行します。Worker は http://localhost:8787 で待ち受けます。
- 同じフォルダーで 2 つ目のターミナルを開き、下記の 2 つのコマンドを実行します: POST は "Order created successfully!" とともに 201 を返し、GET は createdAt を含む注文 123 の JSON を返します。
- POST をもう一度実行します: orderId が既に存在するため、今度は 409 になります。GET を orderId=999 に変えると 404 になります。
- Windows PowerShell 5.1 では curl が Invoke-WebRequest のエイリアスのため、curl の代わりに curl.exe と入力します。
- 1 つ目のターミナルで x または Ctrl+C を押して wrangler dev を停止します。
curl -i -X POST http://localhost:8787/orders -H "Content-Type: application/json" --data "@order.json"
curl -i "http://localhost:8787/orders?orderId=123"8. Cloudflare に deploy
wrangler deploy は src/index.ts をバンドルし、Cloudflare のネットワークに公開します。API Gateway のような stage の概念はなく、deploy ごとに新しいバージョンが作られ、workers.dev の URL がすぐにそのバージョンを配信します。
- npx wrangler deploy を実行します。
- アカウントに workers.dev のサブドメインがまだない場合は、Wrangler が登録を求めます。名前を決めて確定します。
- 表示された URL(https://order-api.<subdomain>.workers.dev の形)を控えます。
npx wrangler deploy9. workers.dev で API をテスト
手順 7 のテストを本番の URL に対して繰り返します。今度は request が Cloudflare のネットワークを通り、リモートのデータベースに書き込まれます。並行して wrangler tail を開き、CloudWatch Logs に相当する request ごとのログを確認します。
- 1 つ目のターミナルで npx wrangler tail を実行し、ログをリアルタイムで表示します。
- 2 つ目のターミナルで、<subdomain> を自分のサブドメインに置き換えてから下記の 2 つのコマンドを実行します。
- POST は 201、GET は注文 123 の JSON を返し、各 request が wrangler tail にログとして表示されます。
- Ctrl+C で wrangler tail を停止します。
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"10. D1 でデータを確認
DynamoDB の Explore items と同様に、ターミナルまたは Dashboard から SQL で D1 を直接照会できます。
- 下記のコマンドを実行し、リモートデータベースの全注文を表示します。
- または Cloudflare Dashboard で D1 を開き、orders-db データベースを選択して、データベースのコンソールで同じ SQL を実行します。
- 注文 123 の item、price、createdAt が正しいことを確認します。
npx wrangler d1 execute orders-db --remote --command="SELECT * FROM orders"11. クリーンアップ
本ラボの Worker とデータベースは無料枠の範囲に収まりますが、アカウントを整理するため、終了後は削除します。
- Worker を削除します: プロジェクトフォルダーで npx wrangler delete を実行し、確認します。
- データベースを削除します: npx wrangler d1 delete orders-db を実行し、確認します。
- このマシンで Wrangler を使わなくなる場合は、npx wrangler logout を実行します。
npx wrangler delete
npx wrangler d1 delete orders-db