bun-cloudflare-workers について
Bun を使って Cloudflare Workers を開発、デプロイするためのスキルガイドです。ローカル開発、テスト、ビルドに加え、KV、D1、Durable Objects、R2 などのバインディングを扱います。
主な機能
- 01Bun を使った Workers プロジェクトの設定
- 02Wrangler によるローカル開発
- 03bun:test と Miniflare によるテスト
- 04KV、D1、Durable Objects、R2 のパターン
- 05ビルドとデプロイの解説
活用シーン
- 01Workers プロジェクトのひな形を作成
- 02Worker をローカルで開発、テスト
- 03Cloudflare のストレージやデータベースのバインディングを利用
- 04Worker をビルドしてデプロイ
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ secondsky/ claude-skills --skill bun-cloudflare-workersインストール後、Claude Code で/bun-cloudflare-workersを入力して読み込まれたか確認します。
詳細
Bun Cloudflare Workers
Bun を使用して Cloudflare Workers を開発・デプロイします。
クイックスタート
# Create new Workers project
bunx create-cloudflare my-worker
cd my-worker
# Install dependencies
bun install
# Development
bun run dev
# Deploy
bun run deploy
安全なインストール
bunx create-cloudflare のようなスキャフォールディングツールは、リモートコードをダウンロードして実行します。実行する前に、サプライチェーンセキュリティのベストプラクティスに従ってください。
- インストール後スクリプトをブロックする — Bun はデフォルトで無効にしています。
package.jsonのtrustedDependenciesで特定のパッケージを許可できます - クールダウン期間 —
bunfig.tomlでminimumReleaseAgeを設定し、新しいバージョンの公開後7日間待機します - インストール前に監査する —
socket package score npm <pkg>を実行するか、socket npm install <pkg>を使用してパッケージを確認します
Socket CLI との統合、クールダウンの設定、ロックファイルの検証、CI での適用など、セキュリティ設定の全容については、dependency-upgrade スキルを読み込んでください。
プロジェクトのセットアップ
package.json
{
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"build": "bun build src/index.ts --outdir=dist --target=browser"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260408.0",
"wrangler": "^4.81.0"
}
}
wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
# Use Bun for local dev
[dev]
local_protocol = "http"
# Bindings
[[kv_namespaces]]
binding = "KV"
id = "xxx"
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxx"
基本的な Worker
// src/index.ts
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/") {
return new Response("Hello from Cloudflare Workers!");
}
if (url.pathname === "/api/data") {
return Response.json({ message: "Hello" });
}
return new Response("Not Found", { status: 404 });
},
};
interface Env {
KV: KVNamespace;
DB: D1Database;
}
Hono の使用
// src/index.ts
import { Hono } from "hono";
type Bindings = {
KV: KVNamespace;
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.get("/", (c) => c.text("Hello Hono!"));
app.get("/api/users", async (c) => {
const users = await c.env.DB.prepare("SELECT * FROM users").all();
return c.json(users.results);
});
app.post("/api/users", async (c) => {
const { name } = await c.req.json();
await c.env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind(name).run();
return c.json({ success: true });
});
export default app;
KV ストレージ
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.searchParams.get("key");
if (request.method === "GET" && key) {
const value = await env.KV.get(key);
return Response.json({ key, value });
}
if (request.method === "PUT" && key) {
const value = await request.text();
await env.KV.put(key, value, { expirationTtl: 3600 });
return Response.json({ success: true });
}
return new Response("Bad Request", { status: 400 });
},
};
D1 データベース
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Query
const { results } = await env.DB.prepare(
"SELECT * FROM users WHERE active = ?"
).bind(1).all();
// Insert
const info = await env.DB.prepare(
"INSERT INTO users (name, email) VALUES (?, ?)"
).bind("Alice", "[email protected]").run();
// Transaction
const batch = await env.DB.batch([
env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Bob"),
env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Charlie"),
]);
return Response.json(results);
},
};
Durable Objects
// src/counter.ts
export class Counter {
private state: DurableObjectState;
private value = 0;
constructor(state: DurableObjectState) {
this.state = state;
this.state.blockConcurrencyWhile(async () => {
this.value = (await this.state.storage.get("value")) || 0;
});
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/increment") {
this.value++;
await this.state.storage.put("value", this.value);
}
return Response.json({ value: this.value });
}
}
// src/index.ts
export { Counter } from "./counter";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const id = env.COUNTER.idFromName("global");
const stub = env.COUNTER.get(id);
return stub.fetch(request);
},
};
# wrangler.toml
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_classes = ["Counter"]
R2 ストレージ
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (request.method === "GET") {
const object = await env.BUCKET.get(key);
if (!object) {
return new Response("Not Found", { status: 404 });
}
return new Response(object.body, {
headers: { "Content-Type": object.httpMetadata?.contentType || "application/octet-stream" },
});
}
if (request.method === "PUT") {
await env.BUCKET.put(key, request.body, {
httpMetadata: { contentType: request.headers.get("Content-Type") || undefined },
});
return Response.json({ success: true });
}
return new Response("Method Not Allowed", { status: 405 });
},
};
Bun を使用した開発
ローカル開発
# Run with wrangler (uses Bun for TypeScript)
bun run dev
# Or directly
bunx wrangler dev
Bun でのテスト
// src/index.test.ts
import { describe, test, expect } from "bun:test";
// Mock worker
const worker = {
async fetch(request: Request) {
return new Response("Hello");
},
};
describe("Worker", () => {
test("returns hello", async () => {
const request = new Request("http://localhost/");
const response = await worker.fetch(request);
expect(await response.text()).toBe("Hello");
});
});
テスト用の Miniflare
import { Miniflare } from "miniflare";
const mf = new Miniflare({
script: await Bun.file("./dist/index.js").text(),
kvNamespaces: ["KV"],
});
const response = await mf.dispatchFetch("http://localhost/");
console.log(await response.text());
本番環境向けビルド
// build.ts
await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "browser", // Workers use browser APIs
minify: true,
sourcemap: "external",
});
bun run build.ts
bunx wrangler deploy
環境変数
# wrangler.toml
[vars]
API_URL = "https://api.example.com"
# Secrets (set via CLI)
# wrangler secret put API_KEY
export default {
async fetch(request: Request, env: Env): Promise<Response> {
console.log(env.API_URL); // From vars (non-secret, safe to log)
// Access a Worker secret (NEVER log it — wrangler tail / Logpush persist logs)
if (env.API_KEY) {
// use env.API_KEY to call the upstream API
}
// ❌ NEVER: console.log(env.API_KEY) — secrets must not appear in logs
return new Response("OK");
},
};
スケジュール実行 Worker(Cron)
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
console.log("Cron triggered at:", event.scheduledTime);
// Perform scheduled task
await env.DB.prepare("DELETE FROM logs WHERE created_at < ?")
.bind(Date.now() - 7 * 24 * 60 * 60 * 1000)
.run();
},
async fetch(request: Request, env: Env): Promise<Response> {
return new Response("OK");
},
};
# wrangler.toml
[triggers]
crons = ["0 * * * *"] # Every hour
よくあるエラー
| エラー | 原因 | 解決方法 |
|---|---|---|
Bun API not available | Workers は V8 を使用します | Web API のみを使用してください |
Module not found | ビルドの問題 | バンドラーの設定を確認してください |
Script too large | 10MB を超えています | バンドルを最適化してください |
CPU time exceeded | 実行時間が長すぎます | 最適化するか、キューを使用してください |
API の互換性
Workers は Web API をサポートしていますが、Bun 固有の API はサポートしていません。
| 利用可能 | 利用不可 |
|---|---|
| fetch() | Bun.file() |
| Response | Bun.serve() |
| Request | bun:sqlite |
| URL | bun:ffi |
| crypto | fs |
| TextEncoder | child_process |
リファレンスを読み込むタイミング
次の場合は references/bindings.md を読み込んでください。
- 高度な KV/D1/R2 のパターン
- キューワーカー
- サービスバインディング
次の場合は references/performance.md を読み込んでください。
- バンドルの最適化
- コールドスタートの短縮
- キャッシュ戦略
コメント
Markdown 対応