はじめに
サポート API では、オープン中のチケットを検索し、個々のレコードを安全に作成、更新、削除する必要があります。この実験では、Worker を D1 に接続し、パラメーター化した SQL アクセスを実装して、API が存在しないレコードや無効な入力をどのように処理するかをテストします。
HTTP ルーターは用意されているため、主な作業はデータベースとの統合です。この独立した実験では、破棄可能な Worker 1 つと D1 データベース 1 つ、およびそれぞれ独立したローカルデータを使用します。
自分の学習用アカウントと新しい VM を使用してください。セットアップでは、まず Node.js 22.22.0 を準備し、/home/labex/project/ticket-database で npm install を実行して、プロジェクト専用の Wrangler 4.131.1 と評価に必要な依存関係をインストールします。依存関係のバージョンは固定されており、インストール時に専用のロックファイルが作成されます。セットアップ中にクラウドへのログインや、評価対象のデータベース操作は行われません。個人のマシンでは、プロジェクト内で npm install --save-dev wrangler@4.131.1 を実行し、同じ Wrangler バージョンをインストールしてください。
この演習では、D1 Free allowances の範囲内で少量の合成レコードを使用します。既存のアカウント使用量もこの上限に含まれます。購入済みのドメインは必要ありません。リソースの削除とログアウトの両方を確認するまで、この VM を保持してください。
この VM を認証し、アカウントを選択する
このステップでは、新しいターミナルを自分の学習用アカウントに接続します。Dashboard にログインしただけでは、VM は認証されません。D1 の権限ではデータベースの作成、SQL の変更、削除が可能になります。Workers の権限ではデプロイが可能になり、KV の権限は Wrangler のクリーンアップ対象の一覧取得を支援します。認証する前に、Background Access を含む実際の同意画面の内容を確認してください。
準備済みのプロジェクトを開き、固定されている CLI のバージョンを確認します。
cd /home/labex/project/ticket-database
npx wrangler --version
4.131.1 と表示されることを確認します。デバイス認証を開始します。--device はブラウザーに入力するコードを表示し、--browser=false はブラウザーを自分で選べるようにします。
npx wrangler login --device --browser=false --scopes account:read user:read d1:write workers_scripts:write workers_kv:write
表示された URL をブラウザーで開き、現在のコードを入力します。自分の学習用アカウントと権限を確認して、認証を許可してください。ターミナルに成功メッセージが表示されるまで待ちます。パスワードやトークンをプロジェクトファイルに貼り付けないでください。
npx wrangler whoami --json
loggedIn: true を確認し、アカウントが 1 つだけ表示される場合でも、アカウントの name と id を読み取ります。対象の ID をコピーし、次の設定に入力してください。次のシェル変数では、他の学習者との衝突を避けるために 6 バイトの乱数(12 個の 16 進数文字)を使用します。ヒアドキュメントによって、JSON の行の間にある JSON が書き込まれます。この中では $RUN が展開されます。
$schema の前のバックスラッシュは、この JSON キーを文字どおり保持します。$RUN は引き続き今回の実行に固有の名前に展開されます。
RUN=labex-c04-d02-$(openssl rand -hex 6)
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "YOUR_ACCOUNT_ID",
"main": "src/index.js",
"compatibility_date": "2026-09-15",
"workers_dev": true,
"preview_urls": false
}
JSON
このブロックを実行する前に、YOUR_ACCOUNT_ID を置き換えてください。RUN を引き続き使用できるよう、このターミナルは開いたままにします。name はこの実行を識別し、account_id はクラウド操作に使用するアカウントを選択します。このファイルは通常の JSON であり、JSONC としても有効です。このファイルを書き込んだだけでは、Worker はデプロイされません。
独立したローカルデータとリモートデータを準備する
このステップでは、D1 への接続を作成し、使い慣れたチケットテーブルに初期データを投入します。src/index.js には HTTP ルーティングと入力検証が用意されています。不足しているストレージ関数は src/store.js にあります。これにより、SQL アクセスに集中できます。
破棄可能なクラウドデータベースを作成します。--binding DB はアプリケーションコードで使用する短い名前を設定し、--update-config は実際の名前と UUID を wrangler.jsonc に記録します。--use-remote=false により、開発時はローカルデータベースを使用します。
npx wrangler d1 create "$RUN-db" --binding DB --update-config --use-remote=false
作成された名前と ID を確認し、保存されたバインディングを調べます。
cat wrangler.jsonc
DB エントリが、この実行で作成したデータベースの名前になっていることを確認します。バインディングは、コードとリソースの間に設定された接続です。その UUID はクラウドデータベースを識別します。一方、--local はこの VM 内の別の SQLite データベースを使用します。SQL コマンドでは、必ず --local または --remote のいずれかを指定してください。
cat schema.sql
npx wrangler d1 execute DB --local --file schema.sql
npx wrangler d1 execute DB --remote --file schema.sql
これで、両方の対象に同じ 2 件の初期チケットが作成されます。DB は、用意されたハンドラーが env.DB として受け取る名前です。設定内のバインディング名と一致している必要があります。
パラメーター化した CRUD を実装する
このステップでは、CRUD(作成、読み取り、更新、削除)を実装します。準備済みステートメントを使うと、SQL の構造と入力値を分離できます。各 ? はパラメーターのプレースホルダーであり、.bind(...) が値を順番に渡します。入力が無害に見える場合でも、ユーザー入力を SQL に連結しないでください。
WHERE は対象となるレコードを制限します。.all() は結果オブジェクトを返し、その results フィールドに行の配列が入ります。.first() は 1 行、または null を返します。SQLite の RETURNING 句を使うと、別の検索を行わずに変更後の行を取得できます。削除では、.run() が meta.changes を公開します。この値によって、レコードが実際に存在したかどうかをルーターが判断できます。
ストレージモジュールを書き込みます。
cat > src/store.js <<'JS'
export async function list(db, status) {
const query = status === null
? db.prepare('SELECT id, subject, status, source FROM tickets ORDER BY id')
: db.prepare('SELECT id, subject, status, source FROM tickets WHERE status = ? ORDER BY id').bind(status);
const { results } = await query.all();
return results;
}
export async function get(db, id) {
return db.prepare('SELECT id, subject, status, source FROM tickets WHERE id = ?').bind(id).first();
}
export async function create(db, subject) {
return db.prepare("INSERT INTO tickets (subject, source) VALUES (?, 'api') RETURNING id, subject, status, source").bind(subject).first();
}
export async function update(db, id, status) {
return db.prepare('UPDATE tickets SET status = ? WHERE id = ? RETURNING id, subject, status, source').bind(status, id).first();
}
export async function remove(db, id) {
const result = await db.prepare('DELETE FROM tickets WHERE id = ?').bind(id).run();
return result.meta.changes === 1;
}
JS
src/index.js を読み、用意されているルーティングがこれらの関数をどのように使用しているか確認します。存在しないレコードは制御された 404 になり、不正な形式の入力は 400 になります。捕捉されたデータベースエラーは、SQL の内部情報を公開せずに 503 になります。
ローカルサーバーをバックグラウンドジョブとして起動し、ターミナルを引き続き使用できるようにします。
npx wrangler dev --ip 0.0.0.0 > dev.log 2>&1 &
起動ログを読み、リスニング開始のメッセージが表示されるまで待ちます。
cat dev.log
curl -i http://localhost:8787/tickets?status=open
HTTP 200 が返り、チケット 1 だけが表示されることを確認します。このサーバーはローカルデータベースを使用しています。クリーンアップで使用するため、ターミナルに表示されたジョブ番号を控えておいてください。
ローカルで書き込みと拒否される入力をテストする
このステップでは、読み取りが成功するケースだけでなく、その他の動作もテストします。curl -i は HTTP ステータスとヘッダーを表示します。-H は JSON の Content-Type を指定し、-d はデフォルトで POST のボディを送信します。
SQL に似た記号を含む subject を作成します。
curl -i http://localhost:8787/tickets -H 'Content-Type: application/json' -d "{\"subject\":\"Printer ' OR 1=1 --\"}"
subject がデータとして保持された状態で、201 が返ることを確認します。返された数値の id を TICKET_ID にコピーしてください。繰り返しテストした後も ID が同じだとは限らないため、ID を推測しないでください。
TICKET_ID=YOUR_RETURNED_ID
curl -i http://localhost:8787/tickets/$TICKET_ID
curl -i -X PATCH http://localhost:8787/tickets/$TICKET_ID -H 'Content-Type: application/json' -d '{"status":"closed"}'
curl -i -X DELETE http://localhost:8787/tickets/$TICKET_ID
curl -i http://localhost:8787/tickets/$TICKET_ID
読み取りでは 200、更新では closed を含む 200、削除ではボディなしの 204、その後の読み取りでは {"error":"not_found"} を含む 404 が返ることを確認します。最初からある 2 件のチケットは変更されずに残っていなければなりません。
不正な JSON と無効な status を送信します。
curl -i http://localhost:8787/tickets -H 'Content-Type: application/json' -d '{'
curl -i -X PATCH http://localhost:8787/tickets/1 -H 'Content-Type: application/json' -d '{"status":"lost"}'
それぞれ invalid_json と invalid_status を含む HTTP 400 が返ることを確認します。SQL のパラメーター化は、入力が SQL として解釈されるのを防ぎます。一方、アプリケーションの検証は、ビジネスルールにない値を拒否します。この 2 つは別の問題を解決します。
バインドされたデータベースにデプロイしてテストする
このステップでは、ハンドラーと D1 バインディングを公開します。データベースにはすでにリモートで初期データが投入されているため、デプロイによってローカルの行がコピーされることはありません。
npx wrangler deploy
デプロイ時に表示された実際の https://...workers.dev URL をコピーし、シェル変数に設定します。これは破棄可能な合成 API なので、テスト後に削除します。
URL='YOUR_DEPLOYED_HTTPS_URL'
curl -i "$URL/tickets?status=open"
200 とチケット 1 が返ることを確認します。新しいデプロイ直後に一時的なプラットフォームエラーが返る場合は、数秒待って、この読み取りを 1 分間まで繰り返してください。ステータスと JSON の両方が一致するまで先に進まないでください。エラーが続く場合は調査が必要です。
リモート API に対して CRUD を繰り返し、返された ID をコピーします。
curl -i "$URL/tickets" -H 'Content-Type: application/json' -d '{"subject":"Remote test"}'
TICKET_ID=YOUR_RETURNED_ID
curl -i -X PATCH "$URL/tickets/$TICKET_ID" -H 'Content-Type: application/json' -d '{"status":"closed"}'
curl -i -X DELETE "$URL/tickets/$TICKET_ID"
curl -i "$URL/tickets/$TICKET_ID"
curl -i "$URL/tickets"
201、200、204、404 の順に返り、最後に変更されていない 2 件の初期チケットが返ることを確認します。Dashboard で、この実行で作成した正確な Worker を開き、Bindings ビューを表示します。DB が自分のデータベースを指していることを確認し、データベースへのリンクをたどって読み取り専用で確認します。保存されたローカルバインディングだけでは、デプロイ済み接続の証拠になりません。

この例では、デプロイ済みの Worker が DB を通じて D1 データベースに接続されています。ランダムな接尾辞はこの実行例を識別するもので、あなたのリソース名は異なります。表の Value リンクは、デプロイ済みバインディングが選択しているデータベースを開きます。
破棄可能なリソースを削除する
このステップでは、VM が認証されたままの状態で、この実験のリソースだけを削除します。まずすべての機能確認を終えてください。削除の確認が完了するまで、設定を保持します。
npx wrangler delete
この実行の設定にある Worker 名だけが対象になっていることを確認します。
npx wrangler d1 delete DB
プロンプトを確認し、この実行で作成したデータベースだけを削除対象にします。その後、データベースを一覧表示します。
npx wrangler d1 list --json
記録しておいたデータベース名と UUID が、成功したレスポンスに存在しないことを確認します。他のリソースは残っていてもかまいません。認証エラーやネットワークエラーでは、削除されたかどうかを判断できません。アクセスを解決してから、次の作業に進む前にもう一度読み取りを実行してください。ログインしたまま、このステップの確認を行います。
ローカル開発ジョブも停止します。ジョブを一覧表示し、自分が開始した wrangler dev ジョブだけを終了します(ジョブ番号が異なる場合は %1 を置き換えてください)。
jobs
kill %1
この VM の認証を終了する
このステップでは、独立した削除確認に成功した後でのみ、認証を終了します。ログアウトすると、この VM に保存された Wrangler の認証情報が削除されます。VM を閉じただけでは、クラウドのクリーンアップにはなりません。
npx wrangler logout
npx wrangler whoami --json
loggedIn: false と表示されることを確認します。この未認証のクエリは、ゼロ以外の終了コードになる場合があります。構造化されたレスポンスにログアウト済みであることが明示されている場合に限り、その状態は想定どおりです。確認を完了してから、実験環境を閉じてください。
まとめ
Worker にチケット検索を追加する方法を実践しました。データベースの結果を実際に確認し、選択したアカウントとローカル状態を明示的に管理し、ログアウトする前に破棄可能なリソースを削除しました。



