サポートリクエスト API を構築する

CloudflareBeginner
オンラインで実践に進む

はじめに

サポートフォームには、正しいリクエスト、不正な JSON、存在しないルート、利用できないチケットサービスを区別できる API が必要です。この実験では、JavaScript で HTTP 境界を構築し、ローカルでテストした後、使い捨ての上流 Worker と一緒にデプロイします。

自分の Cloudflare 学習用アカウントと、接続実験で学んだデバイス認証の知識を使用してください。この実験は、Node.js 22.22.0 とプロジェクトローカルの Wrangler 4.131.1 が /home/labex/project/support-api にインストールされた新しい VM から開始します。基本的な JavaScript の関数、オブジェクト、モジュールの知識が前提です。HTTP の動作と非同期リクエストについては、この実験で説明します。2 つの公開 Worker は合成データだけを使用します。用意されている上流サービスはリクエストを受け付けますが、何も保存しません。これは永続的なチケットシステムではありません。この小規模な演習には Workers Free と workers.dev サブドメインで十分です。データベース、購入済みドメイン、有料アップグレードは必要ありません。リクエストはアカウントの Workers 使用量として計上されます。

実験の終了前に、2 つの Worker を削除してログアウトします。リソース名と URL に使用するシェル変数を維持するため、同じターミナルを開いたままにしてください。

パスとメソッドでリクエストをルーティングする

このステップでは、サポートする各 URL に明示的なメソッドとレスポンスを設定します。パスは操作を識別し、メソッドはアクションを示します。GET /health は利用可能かどうかを確認し、POST /requests はサポートリクエストを受け付けます。

用意されたプロジェクトに移動し、ツールを確認します。

cd /home/labex/project/support-api
node --version
npx wrangler --version

Node は v22.22.0、Wrangler は 4.131.1 と表示されるはずです。インストールはすでに完了しています。自分のコンピューターで実行する場合は、Node をインストールし、プロジェクト内で npm install --save-dev wrangler@4.131.1 を実行します。ロックファイルを使ってプロジェクトを再現するときは npm ci を使用します。

重複しない使い捨ての名前を生成します。openssl rand -hex 6 は 12 個のランダムな 16 進数文字を出力します。$(...) はその出力を埋め込み、シェルの代入によって後続のコマンドで使えるよう保存します。

WORKER_NAME="labex-support-$(openssl rand -hex 6)"

標準的な Wrangler 設定を書き込みます。cat > file <<MARKER は、終了マーカーまでの行を書き込みます。マーカーを引用符で囲んでいないため、シェルは $WORKER_NAME を置き換えます。

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false,
  "vars": {"UPSTREAM_URL": "http://127.0.0.1:8081"}
}
CONFIG

main はハンドラーを指定し、compatibility_date はランタイムの動作を選択します。varsenv 経由で秘密ではない上流アドレスを提供します。ここでは、後のステップで起動するローカルのフィクスチャを指します。リソースを簡単に管理できるよう、公開プレビュー URL は無効にしています。

ハンドラーを書き込みます。引用符で囲んだ JS マーカーは JavaScript をそのまま保持します。new URL(...).pathname はルートを取り出します。三項演算子は許可するメソッドを選択します。HTTP 405 レスポンスでは Allow にそのメソッドも示します。Response.json はオブジェクトをシリアライズし、コンテンツタイプを設定します。ハンドラーを async にしておくことで、後のステップで非同期処理を await できます。

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    return Response.json({error: 'not_implemented'}, {status: 501});
  }
};
JS

ローカルの Wrangler をバックグラウンドで起動します。> は出力をリダイレクトし、2>&1 はエラーも含め、& はサーバーを実行したままターミナルのプロンプトを返します。

npx wrangler dev --port 8080 > api.log 2>&1 &
cat api.log

ログにポート 8080 で準備完了したことが表示されるまで待ちます。起動中の場合は cat api.log をもう一度実行してください。curl -i は HTTP ステータスとヘッダーを含めて表示します。

curl -i http://127.0.0.1:8080/health
curl -i http://127.0.0.1:8080/missing
curl -i http://127.0.0.1:8080/requests

それぞれ、{"status":"ok"} を含む 200、{"error":"not_found"} を含む 404、{"error":"method_not_allowed"}Allow: POST を含む 405 が返るはずです。これらのエラーレスポンスは意図したものです。サーバーを起動したまま検証ボタンを使用してください。

JSON 入力を解析して検証する

このステップでは、上流サービスを呼び出す前に不正な入力を拒否します。HTTP 415 はメディアタイプがサポートされていないこと、400 は JSON を解析できないこと、422 は解析したデータが契約を満たしていないことを示します。subject は文字列で、空白を削除した後に 1~80 文字でなければなりません。

ハンドラーを次の完全な内容に置き換えます。headers.get は宣言されたメディアタイプを読み取ります。; で分割することで、charset パラメーターを許可しています。await request.json() は解析が完了するまで待機し、本文を一度読み取ります。try/catch は解析例外を予測可能なレスポンスに変換します。JSON では null、配列、数値も表現できるため、文字列メソッドを使う前にデータの形を検証します。trim() は受け付ける subject を正規化します。

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    const mediaType = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
    if (mediaType !== 'application/json') {
      return Response.json({error: 'unsupported_media_type'}, {status: 415});
    }
    let body;
    try {
      body = await request.json();
    } catch {
      return Response.json({error: 'invalid_json'}, {status: 400});
    }
    if (!body || Array.isArray(body) || typeof body.subject !== 'string' ||
        body.subject.trim().length < 1 || body.subject.trim().length > 80) {
      return Response.json({error: 'invalid_subject'}, {status: 422});
    }
    const subject = body.subject.trim();
    return Response.json({subject}, {status: 201});
  }
};
JS

ソースが変更されると Wrangler は再読み込みします。cat api.log でコンパイルエラーがないか確認してください。有効なリクエストを送信します。-H はヘッダーを指定し、--data は本文を指定して POST を選択します。シングルクォートによって、シェル内の JSON のダブルクォートをそのまま保持できます。

curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"  Printer offline  "}'

201 と {"subject":"Printer offline"} が返るはずです。これはメモリ上の確認応答であり、保存されたチケットではありません。3 種類の拒否パスを確認します。

curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"   "}'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: text/plain" --data 'hello'

400 invalid_json、422 invalid_subject、415 unsupported_media_type が返るはずです。JSON の null[]{"subject":5} も試してください。いずれも例外を発生させず、422 を返さなければなりません。検証ボタンを使用してください。検証ではこれらの境界条件に加えて、ヘルスチェックとルーティングの動作も確認します。

上流サービスを呼び出し、エラーを封じ込める

このステップでは、API を用意されたチケットサービスのシミュレーターに接続します。上流サービスとは、サービスから呼び出される依存先です。シミュレーターは通常の subject に対して合成チケットを 1 件返し、特別な subject である simulate-outage に対して HTTP 503 を返します。リクエストを保存することはありません。

用意されたソースを確認してフィクスチャの内容を理解し、その後、専用の一意な Worker 名を設定します。

cat upstream/index.js
cat > upstream/wrangler.jsonc <<CONFIG
{
  "name": "${WORKER_NAME}-upstream",
  "main": "index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false
}
CONFIG

--config は 2 つ目の設定ファイルを選択します。2 つのローカル Worker を同時に実行できるよう、ポート 8081 と別のインスペクターポートを使用します。

npx wrangler dev --config upstream/wrangler.jsonc --port 8081 --inspector-port 9230 > upstream.log 2>&1 &
cat upstream.log
curl -i http://127.0.0.1:8081/health

準備完了するまで待ち、{"service":"support-upstream","status":"ok"} を含む 200 が返ることを確認します。次に、メインハンドラーを統合版全体に置き換えます。グローバルの fetch 関数は外部リクエストを作成し、JSON.stringify は検証済みの subject をエンコードします。await はレスポンスを待ちます。HTTP エラーでは例外が発生しないため、upstream.ok でステータスを明示的に確認します。catch は接続失敗や JSON レスポンスを読み取れない場合を別に処理します。HTTP 502 によって、内部レスポンス本文を公開せず、依存先が失敗したことをクライアントに伝えます。

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    const mediaType = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
    if (mediaType !== 'application/json') {
      return Response.json({error: 'unsupported_media_type'}, {status: 415});
    }
    let body;
    try {
      body = await request.json();
    } catch {
      return Response.json({error: 'invalid_json'}, {status: 400});
    }
    if (!body || Array.isArray(body) || typeof body.subject !== 'string' ||
        body.subject.trim().length < 1 || body.subject.trim().length > 80) {
      return Response.json({error: 'invalid_subject'}, {status: 422});
    }
    const subject = body.subject.trim();
    try {
      const upstream = await fetch(`${env.UPSTREAM_URL}/tickets`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({subject})
      });
      if (!upstream.ok) {
        return Response.json({error: 'upstream_unavailable'}, {status: 502});
      }
      const ticket = await upstream.json();
      return Response.json({ticket: ticket.ticket, subject}, {status: 201});
    } catch {
      return Response.json({error: 'upstream_unavailable'}, {status: 502});
    }
  }
};
JS
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"Printer offline"}'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"simulate-outage"}'

最初のリクエストでは {"ticket":"demo-1001","subject":"Printer offline"} を含む 201、次のリクエストでは {"error":"upstream_unavailable"} を含む 502 が返るはずです。シミュレーター内部の診断情報が表示されてはいけません。subject は合成データであり、このエンドポイントには永続的な副作用はありません。2 つのローカルサーバーを起動したまま、検証ボタンを使用してください。

この実験では、外部サービスとの境界を学ぶために通常の HTTP を使用します。Worker 間の内部呼び出しに使うサービスバインディングについては、後の実験で扱います。時間制限と詳細な診断については、「Diagnose Worker Failures」で学びます。フィクスチャはサイズを制限した小さなレスポンスを返します。実際の API では、信頼できないリクエストとレスポンスのサイズも制限する必要があります。

公開 API をデプロイして実行する

このステップでは、両方の Worker を同じ学習用アカウントにデプロイし、ローカルの上流アドレスを公開 URL に置き換えます。まず、2 つのローカルジョブを停止します。jobs の結果を確認し、実際のジョブ番号を使ってください。以下の例では、API が 1、上流が 2 であると仮定しています。

jobs
kill %1 %2

この新しい VM を認証します。認証の許可によってアカウントが識別され、Worker のデプロイと削除が可能になります。末尾の権限はデプロイ実験の許可に合わせていますが、この実験ではログストリームは必要ありません。

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_tail:read

表示されたブラウザーリンクを開き、現在のデバイスコードを入力します。Wrangler の権限(必須の Background Access を含む)を確認し、自分の学習用アカウントだけを選択して認証します。ターミナルに戻り、完了するまで待ちます。

npx wrangler whoami --json

loggedIn: true、アカウント名、accounts 内の実際の ID を確認します。以下の YOUR_ACCOUNT_ID をその ID に置き換えてください。ステップ 1 で生成した名前をそのまま使用します。変数を失った場合は、保存済みの設定を読み取って復元し、別のリソース名を生成しないでください。

cat > upstream/wrangler.jsonc <<CONFIG
{
  "name": "${WORKER_NAME}-upstream",
  "main": "index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
npx wrangler deploy --config upstream/wrangler.jsonc

デプロイ出力に表示された正確な workers.dev URL をコピーします。アカウントにすでにあるサブドメインを再利用してください。Wrangler が初回のサブドメイン登録を提示した場合は、利用可能な名前を選び、確認手順に従います。既存のアカウントサブドメインは変更しないでください。

次に、アカウント ID と上流 URL の両方のプレースホルダーを置き換えて、メイン設定を書き直します。上流 URL には末尾のスラッシュを付けません。global_fetch_strictly_public によって、同じアカウントの workers.dev サブドメイン上にあるもう一方の Worker を含め、外部向けの fetch() が公開インターネット経由でルーティングされます。この設定がないと、両方の Worker が個別には動作していても、同一ゾーン内の HTTP 呼び出しが失敗する場合があります。このフラグはデプロイする API の設定に含めます。先ほどのローカルループバックフィクスチャには必要ありません。詳しくは Fetch API のガイダンス を参照してください。

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-09-14",
  "compatibility_flags": ["global_fetch_strictly_public"],
  "workers_dev": true,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID",
  "vars": {"UPSTREAM_URL": "YOUR_UPSTREAM_URL"}
}
CONFIG
cat wrangler.jsonc
npx wrangler deploy

メイン API のデプロイ出力から URL をコピーし、以下の変数に設定します。

API_URL="https://YOUR_API.YOUR_SUBDOMAIN.workers.dev"
curl -i "$API_URL/health"
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{"subject":"Printer offline"}'
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{"subject":"simulate-outage"}'
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{'

ローカルと同じ契約が確認できるはずです。つまり、ヘルスチェックは 200、合成チケットは 201、上流エラーは 502、不正な JSON は 400 です。接続エラーが発生した場合は、ホスト名の反映を待ってから再試行してください。Dashboard で同じ学習用アカウントを選択し、Compute → Workers & Pages を開きます。2 つの正確な名前を見つけ、デプロイ出力に表示されたアドレスと比較します。これは読み取り専用の確認です。そこから重複するアプリケーションを作成しないでください。

次の例では、メイン API と対応する -upstream サービスを示しています。左側のサイドバーで Compute を展開し、Workers & Pages を選択します。アカウントに他のプロジェクトがある場合は Search applications を使用してください。生成された完全な名前とその下に表示されるアドレスを、2 つのデプロイ出力と比較します。ランダムなサフィックスとアカウントのサブドメインは、この例とは異なります。

サポート API と対応する上流 Worker を表示する Workers and Pages

両方のリソースが、選択した同じアカウントに表示されるはずです。リソースの存在によってデプロイ先が確認でき、上記の HTTP レスポンスによって API が動作しているかどうかが確認できます。いずれかの名前が見つからない場合は、アカウントセレクターとデプロイ出力を確認してから再試行してください。CLI でデプロイしたアプリケーションを重複させるために Create application を使用しないでください。

検証ボタンを使用してください。検証では、両方の Worker の所有権、デプロイされた上流バインディング、公開エンドポイントからの成功レスポンスと失敗レスポンスを個別に確認します。この実験のシミュレーターに対して、合成されたステートレスなリクエストだけを送信します。

2 つの使い捨て Worker を削除する

このステップでは、認証が有効なうちに API と上流 Worker を削除し、結果を確認します。この実験で作成するクラウドリソースはこの 2 つだけです。削除する前に、両方の設定を確認します。

cat wrangler.jsonc
cat upstream/wrangler.jsonc

メインの labex-support-... 名と、それに対応する -upstream サフィックスが付いていることを確認します。学習用アカウント ID も同じでなければなりません。最初にメイン API を削除し、次に上流 Worker を削除します。各確認プロンプトで正確な名前を確認し、キー y を 1 回だけ押してください。

npx wrangler delete
npx wrangler delete --config upstream/wrangler.jsonc

Wrangler 4.131.1 では、Worker を削除した後、従来の Workers Sites KV データを確認する際に認証エラーが表示されることがあります。これは、この認証許可に KV へのアクセス権がないためです。この特定の診断メッセージだけでは、削除の成功も失敗も証明できません。メッセージを消すためだけに追加の権限を付与しないでください。Workers & Pages を更新して検証ボタンを使用します。認証済みのインベントリ確認で、両方の名前が存在しないことを確認できなければなりません。ネットワークエラーや認証エラーでは判断できないため、続行する前に解決してください。他のアプリケーション、学習用アカウント、そのサブドメインは保持してください。

VM の接続を解除する

このステップでは、2 つのリソースのクリーンアップ確認が完了した後、この VM の Wrangler 認証を削除します。ログアウトしても Worker は削除されないため、先にクリーンアップを行いました。

npx wrangler logout
npx wrangler whoami --json

明示的に "loggedIn": false と表示されるはずです。認証されていない状態でのステータスコマンドは、構造化された結果でログアウトが明確に示されていれば、ゼロ以外の終了コードになることがあります。これは想定された動作です。ネットワークエラーは同じ意味ではありません。検証ボタンを使用してから、LabEx 環境を終了してください。ブラウザーのログイン状態と学習用アカウントは、後の実験でも利用できます。新しい VM では、その VM 用の認証が必要になります。

まとめ

メソッドを認識する HTTP API を構築し、JSON を解析して検証し、受け付けたデータを正規化し、上流サービスの障害を予測可能な公開エラーに変換しました。ローカルと Cloudflare 上で正常なリクエストと拒否されるリクエストをテストし、2 つのデプロイの所有権を確認し、使い捨てのリソースを削除して、VM の接続を解除しました。

詳しくは、公式の Request APIResponse APIFetch API を参照してください。