Wrangler CLIで作るCloudflare API: D1とKVによる高速エッジ開発
Cloudflare Workers、Wrangler CLI、D1 SQLデータベース、KVキャッシュを連携させた高速サーバーレスAPIの開発とデプロイ手順を徹底解説。
従来、グローバルにスケールする分散APIを構築するには、複数リージョンのコンテナ管理やデータベースのレプリケーション設定、複雑なリバースプロキシの調整が必要でした。
しかし、Cloudflareのエッジプラットフォームと Wrangler CLI を活用すれば、リレーショナルデータベース(D1)と超高速キャッシュ(KV)を統合したフルスタックREST APIをわずか数分で作成し、世界300箇所以上のデータセンターにゼロコールドスタートで即座にデプロイできます。
1. 全体像の理解(子どもでもわかる解説)#
システムを「一流のレストラン」に例えてみましょう:
- Cloudflare Worker(ウェイター / シェフ): お客様のリクエストを受け取り、一番近い店舗(エッジ)でビジネスロジックを実行して素早く料理を提供します。
- Cloudflare KV(作り置きのスピードカウンター / キャッシュ): よく注文される人気の料理を置いておく場所です。注文が入ると、厨房に行かずに一瞬で料理を渡せます(超高速な読み取り・1ms未満のレイテンシ)。
- Cloudflare D1(メインの食材庫とレシピ帳 / データベース): 全てのデータが確実に保存されるSQLiteベースのサーバーレスデータベースです。正確なSQLクエリで安全にデータを管理します。
graph TD
Client["📱 クライアント (Client Request)"] -->|HTTP GET/POST| Worker["⚡ Cloudflare Worker (Edge API)"]
Worker -->|1. キャッシュ確認| KV["⚡ Cloudflare KV (超高速Readキャッシュ)"]
KV -.->|Cache Hit - 即座に応答| Worker
KV -.->|Cache Miss| D1
Worker -->|2. データ検索/更新| D1["🗄️ Cloudflare D1 (Serverless SQLite)"]
D1 -->|3. キャッシュ書き込み| KV
Worker -->|HTTP JSON Response| Client
2. プロジェクトの準備と初期化#
Node.js ↗ または Bun ↗ がインストールされていること、およびCloudflareアカウントが準備されていることを確認してください。
ステップ1: Workerプロジェクトの作成#
npm create cloudflare@latest コマンドでTypeScriptテンプレートを作成します:
# TypeScriptテンプレートでWorkerを作成
npm create cloudflare@latest edge-api -- --type=hello-world-typescript --ts --git --deploy=false
cd edge-apibash最新のWrangler CLIをローカルにインストールします:
npm install -D wranglerbashCloudflareアカウントへCLIログインを実行します:
npx wrangler loginbash3. Cloudflare D1(SQLデータベース)のセットアップ#
Cloudflare D1はSQLiteをベースにした分散サーバーレスリレーショナルデータベースです。
ステップ1: D1データベースの作成#
以下のコマンドで ecommerce-db という名前のD1データベースを作成します:
npx wrangler d1 create ecommerce-dbbashコマンド完了後、設定用のバインディング情報が表示されます:
[[d1_databases]]
binding = "DB"
database_name = "ecommerce-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"textステップ2: スキーマ定義とマイグレーションの実行#
プロジェクトルートに schema.sql を作成します:
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO products (id, name, price) VALUES
('prod_1', 'ワイヤレスメカニカルキーボード', 129.99),
('prod_2', 'ウルトラワイドゲーミングモニター', 499.99),
('prod_3', '昇降式エルゴノミクスデスク', 349.50);sqlローカル開発環境でスキーマを実行します:
npx wrangler d1 execute ecommerce-db --local --file=./schema.sqlbash本番(リモート)データベースでスキーマを実行します:
npx wrangler d1 execute ecommerce-db --remote --file=./schema.sqlbash4. Cloudflare KV(Key-Valueキャッシュ)のセットアップ#
Cloudflare KVは高スループット・低遅延の読み取りに最適化されたグローバル分散ストレージです。
ステップ1: KVネームスペースの作成#
本番用とテスト/プレビュー用の2つのネームスペースを作成します:
# 本番用ネームスペース
npx wrangler kv namespace create CACHE_KV
# プレビュー用ネームスペース
npx wrangler kv namespace create CACHE_KV --previewbash出力されるID情報を控えておきます:
[[kv_namespaces]]
binding = "CACHE_KV"
id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
preview_id = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"text5. wrangler.toml の設定#
wrangler.toml にD1とKVのバインディングを追加します:
name = "edge-api"
main = "src/index.ts"
compatibility_date = "2026-08-01"
# D1 Databaseのバインド
[[d1_databases]]
binding = "DB"
database_name = "ecommerce-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# KV Cacheのバインド
[[kv_namespaces]]
binding = "CACHE_KV"
id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
preview_id = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"tomlバインディング用のTypeScript型定義を自動生成します:
npx wrangler typesbash6. Cache-AsideパターンによるAPIの実装#
src/index.ts にAPIロジックを実装します。Cache-Asideパターン を適用します:
GET /api/products/:id受信時、まずKVキャッシュを確認。- キャッシュが存在する場合(HIT):即座にレスポンスを返却。
- キャッシュが存在しない場合(MISS):D1から取得し、TTLを設定してKVに書き込んだ上で返却。
POST /api/products受信時:D1にデータを新規保存し、KVキャッシュも更新。
export interface Env {
DB: D1Database;
CACHE_KV: KVNamespace;
}
interface Product {
id: string;
name: string;
price: number;
created_at?: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
const method = request.method;
// Route: GET /api/products/:id
if (method === 'GET' && path.startsWith('/api/products/')) {
const id = path.replace('/api/products/', '');
const cacheKey = `product:${id}`;
// 1. KVキャッシュを確認
const cached = await env.CACHE_KV.get(cacheKey, 'json');
if (cached) {
return Response.json(
{ source: 'kv-cache', data: cached },
{ headers: { 'X-Cache-Status': 'HIT', 'Content-Type': 'application/json' } }
);
}
// 2. キャッシュミス: D1から取得
const product = await env.DB.prepare('SELECT * FROM products WHERE id = ?')
.bind(id)
.first<Product>();
if (!product) {
return Response.json({ error: '商品が見つかりません' }, { status: 404 });
}
// 3. バックグラウンドでKVに60秒の有効期限(TTL)で保存
ctx.waitUntil(
env.CACHE_KV.put(cacheKey, JSON.stringify(product), {
expirationTtl: 60,
})
);
return Response.json(
{ source: 'd1-database', data: product },
{ headers: { 'X-Cache-Status': 'MISS', 'Content-Type': 'application/json' } }
);
}
// Route: POST /api/products
if (method === 'POST' && path === '/api/products') {
const body = (await request.json()) as Partial<Product>;
if (!body.name || !body.price) {
return Response.json({ error: '商品名または価格が不足しています' }, { status: 400 });
}
const id = `prod_${Date.now()}`;
await env.DB.prepare('INSERT INTO products (id, name, price) VALUES (?, ?, ?)')
.bind(id, body.name, body.price)
.run();
const newProduct: Product = { id, name: body.name, price: body.price };
// KVキャッシュを更新
await env.CACHE_KV.put(`product:${id}`, JSON.stringify(newProduct), {
expirationTtl: 300,
});
return Response.json({ success: true, product: newProduct }, { status: 201 });
}
return new Response('Edge API は正常に稼働中です!', { status: 200 });
},
};typescript[!TIP] キャッシュ保存処理を
ctx.waitUntil(...)に委ねることで、非同期書き込み完了を待たずにクライアントへ最速でレスポンスを返せます。
7. ローカル検証と本番デプロイ#
ローカル開発サーバーの起動#
ローカル環境でD1とKVをエミュレートして実行します:
npx wrangler devbashcurl コマンドで動作を確認します:
# 1回目のリクエスト(MISS - D1から読み込み)
curl http://localhost:8787/api/products/prod_1
# 2回目のリクエスト(HIT - KVキャッシュから即座に返却)
curl http://localhost:8787/api/products/prod_1bash本番環境へのデプロイ#
世界中のCloudflareエッジネットワークへ1コマンドでデプロイします:
npx wrangler deploybashデプロイ完了後、https://edge-api.<subdomain>.workers.dev のようなURLが発行され、世界300以上の都市から即座に利用可能になります!
8. Rust と Axum を使用する場合の違い#
TypeScriptの代わりにRustの厳格な型安全性と高パフォーマンスを求める場合、Cloudflare WorkersはRustコードをWebAssembly(wasm32-unknown-unknown)にコンパイルし、worker-rs ↗ を介して Axum ↗ フレームワークをエッジ上で動作させることができます。
アーキテクチャ比較: TypeScript vs Rust (Axum)#
graph LR
subgraph TS["TypeScript Worker"]
direction TB
TS_Code["src/index.ts"] --> V8["V8 Isolate エンジン<br/>(ネイティブJSバインディング)"]
V8 --> DB_TS["env.DB / env.CACHE_KV"]
end
subgraph RS["Rust + Axum Worker"]
direction TB
RS_Code["src/lib.rs (Axum Router)"] --> WASM["WASM モジュール<br/>(wasm32-unknown-unknown)"]
WASM --> WRS["worker-rs FFI ブリッジ"]
WRS --> DB_RS["env.d1('DB') / env.kv('CACHE_KV')"]
end
| 比較項目 | TypeScript | Rust (Axum + worker-rs) |
|---|---|---|
| コンパイル対象 | JavaScript (V8 Isolate上で直接実行) | WebAssembly (.wasm) worker-build経由 |
| ルーティング構成 | 標準の fetch(request, env, ctx) | axum::Router と Extractor (State, Path, Json) |
| バインディング取得 | ネイティブオブジェクト: env.DB, env.CACHE_KV | worker::Env FFI経由: env.d1("DB")?, env.kv("CACHE_KV")? |
| パフォーマンスと安全性 | 迅速なプロトタイピング、ビルド時型チェック | メモリ安全性、ゼロコスト抽象化、コンパイル済みWASM |
wrangler.toml 設定 | main = "src/index.ts" | main = "build/worker/shim.mjs" + [build] command = "worker-build --release" |
Rust Axum による D1 & KV 実装例#
Rustでは、Workerのエントリポイント内で axum::Router を生成し、Axumの State を通じて worker::Env を共有します:
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tower_service::Service;
use worker::*;
#[derive(Clone)]
struct AppState {
env: Arc<worker::Env>,
}
#[derive(Serialize, Deserialize)]
struct Product {
id: String,
name: String,
price: f64,
}
// GET /api/products/:id (KVキャッシュ優先 & D1フォールバック)
async fn get_product(
Path(id): Path<String>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let kv = state.env.kv("CACHE_KV").map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let cache_key = format!("product:{}", id);
// 1. KVキャッシュの確認
if let Ok(Some(cached_json)) = kv.get(&cache_key).text().await {
return Ok((
StatusCode::OK,
[("X-Cache-Status", "HIT"), ("Content-Type", "application/json")],
cached_json,
));
}
// 2. キャッシュミス: D1データベースへのクエリ
let d1 = state.env.d1("DB").map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let statement = d1.prepare("SELECT id, name, price FROM products WHERE id = ?1").bind(&[&id.into()])
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let product = statement.first::<Product>(None).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
.ok_or_else(|| (StatusCode::NOT_FOUND, "商品が見つかりません".to_string()))?;
let json_str = serde_json::to_string(&product)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// 3. KVキャッシュへTTL 60秒で保存
let _ = kv.put(&cache_key, &json_str).unwrap().expiration_ttl(60).execute().await;
Ok((
StatusCode::OK,
[("X-Cache-Status", "MISS"), ("Content-Type", "application/json")],
json_str,
))
}
#[event(fetch)]
async fn fetch(req: HttpRequest, env: Env, _ctx: Context) -> Result<axum::http::Response<axum::body::Body>> {
let state = AppState { env: Arc::new(env) };
let mut router = Router::new()
.route("/api/products/:id", get(get_product))
.with_state(state);
let response = router.call(req).await.unwrap();
Ok(response)
}rust9. Wrangler CLI 主要コマンド早見表#
| コマンド | 説明 |
|---|---|
wrangler login | Cloudflareアカウントとの認証 |
wrangler dev | D1 & KV対応ローカルエミュレータ起動 |
wrangler d1 create <name> | 新規D1データベースの作成 |
wrangler d1 execute <name> --file=./schema.sql | SQLファイルの実行(--local または --remote) |
wrangler kv namespace create <name> | 新規KVネームスペースの作成 |
wrangler types | TypeScript型定義の自動生成 |
wrangler deploy | Workerをグローバル本番環境へデプロイ |
wrangler tail | 本番環境のリアルタイムログをターミナルで確認 |