Cloudflare Workers¶
Cloudflare Workers Hono Validation
Hono で動作する Cloudflare Workers サービスで、動作する Workers KV の例が組み込まれている。同梱のアプリは "hello world" を超えており、POST /greetings で送信された挨拶文を KV ネームスペースに保存し、GET /greetings で読み戻す。そのため wrangler dev を実行した瞬間、プレースホルダではなく実際にバインディングを動かせる。
ランタイムは Node ではなく V8 isolate である。テンプレートは @hono/node-server ではなく Hono のネイティブ fetch エクスポートを使用しており、wrangler.jsonc (注意: .json ではなく .jsonc) が KV バインディングを事前宣言しているため、wrangler dev を初めて実行した際に開発セッションが実際のローカルストア付きで起動する。
生成内容¶
my-project/
├── rescript.json
├── package.json
├── wrangler.jsonc # Workers config: name, main, compatibility_date, kv_namespaces
├── src/
│ ├── Server.res # Hono app + POST/GET /greetings; `export default app`
│ ├── Kv.res # Workers KV bindings (get / put / list)
│ ├── Hono.res # Hono bindings
│ ├── Validation.res # zod or sury — selected in the wizard
│ └── __tests__/Server.test.mjs # vitest smoke import
├── README.md # API / KV Setup / Deploy
├── LICENSE # MIT, holder = project name
├── .nvmrc # Node 24 (for tooling — runtime is V8 isolates)
├── .gitignore # node_modules, .wrangler/, dist/, .dev.vars
├── .editorconfig # 2-space indent, LF line endings
└── .github/
├── dependabot.yml # weekly npm updates
└── workflows/ci.yml # install + rescript build + vitest
ウィザードオプション¶
オプション |
効果 |
|---|---|
Project name |
npm の |
Package manager |
npm / pnpm / yarn / bun。 |
Validation library |
|
主要な依存¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ |
|
|
標準ライブラリ |
|
|
コンパイル済み |
|
|
Workers ネイティブの fetch ハンドラを備えた HTTP ルーター |
|
|
リクエストボディのバリデーション (ウィザードで選択) |
|
|
Cloudflare CLI: 開発サーバー、KV、デプロイ |
|
|
スモークテストランナー |
|
|
|
|
@hono/node-server は 同梱されない — Workers ランタイムが Hono のネイティブ fetch エクスポートを直接受け取る (Server.res を参照)。
主要なファイル¶
wrangler.jsonc¶
Workers の設定ファイル。コメントが保持されるため (これが .jsonc である理由)、KV セットアップのワークフローが、説明対象のバインディングの隣に置かれる:
{
"name": "my-project",
"main": "src/Server.res.mjs",
"compatibility_date": "2024-01-01",
"kv_namespaces": [
{
"binding": "GREETINGS",
"id": "REPLACE_WITH_PRODUCTION_KV_NAMESPACE_ID",
"preview_id": "REPLACE_WITH_PREVIEW_KV_NAMESPACE_ID"
}
]
}
id は wrangler deploy (本番) が使用し、preview_id は wrangler dev (ローカル) が使用する。両者を分けておくことで、ローカルでの実験が本番データを変更することがない — 作成と配線の完全なワークフローについては README の KV Setup セクションを参照してほしい。
src/Server.res¶
Hono アプリ。env はこの Worker が実際に使用するバインディング (GREETINGS) に絞り込まれており、ファイルの末尾は %%raw("export default app") になっている。これは Workers ランタイムがデフォルトエクスポートの fetch(request, env, ctx) を呼び出すためである:
type env = {"GREETINGS": Kv.namespace}
let app = Hono.createApp()
app->Hono.get("/", ctx => ctx->Hono.text("Workers + Hono + ReScript"))
app->Hono.post("/greetings", async ctx => {
let env: env = %raw("ctx.env")
let raw = await ctx->Hono.req->Hono.jsonBody
switch Validation.parseGreetingPayload(raw) {
| Error(msg) => ctx->Hono.status(400)->Hono.json({"error": msg})
| Ok(payload) =>
await env["GREETINGS"]->Kv.put(payload.name, Date.now()->Float.toString)
ctx->Hono.status(201)->Hono.json({"ok": true, "name": payload.name})
}
})
app->Hono.get("/greetings", async ctx => {
let env: env = %raw("ctx.env")
let result = await env["GREETINGS"]->Kv.list
ctx->Hono.json({"names": result.keys->Array.map(k => k["name"])})
})
%%raw("export default app")
src/Kv.res¶
Workers KV API のバインディング。同梱のサブセット (get / put / list) はサンプルを動かすには十分である。このファイルは、必要になった際に Durable Objects、R2、queues、D1 といった追加バインディングを書き始めるための雛形として機能する:
type namespace
type listResult = {keys: array<{"name": string}>}
@send external get: (namespace, string) => promise<Nullable.t<string>> = "get"
@send external put: (namespace, string, string) => promise<unit> = "put"
@send external list: namespace => promise<listResult> = "list"
namespace 型は意図的に opaque である — Cloudflare が実行時に env.GREETINGS 経由で実際の KV インスタンスを注入し、ルートハンドラ内では %raw("ctx.env") を介して読み取る。
src/Validation.res¶
両バリアントは parseGreetingPayload: JSON.t => result<greetingPayload, string> を公開する。ルートハンドラは Error で 400 を返し、Ok で処理を続行する — 例外伝搬も追加のミドルウェアもない。より広いエコシステム (zod-to-openapi、hono/zod-openapi、drizzle-zod) を取りたいなら zod を、ReScript ネイティブの使い心地と小さいランタイムフットプリントを優先するなら sury を選ぶとよい。
src/__tests__/Server.test.mjs¶
モジュールがロードされることを検証する 1 行のスモークテスト:
await expect(import("../Server.res.mjs")).resolves.toBeDefined();
これは Miniflare や unstable_dev を配線せずとも、CI が明らかなリンク時のリグレッション (import 漏れ、壊れた %raw ブロック) を捕捉できるようにするためのものである。ルートにカバーする価値のあるロジックが入ってきたら、KV バインドの統合テストを追加するとよい — wrangler は vitest 向けに、実際の Workers ランタイムを in-process で起動する unstable_dev API を提供している。
エンドポイント¶
エンドポイント |
メソッド |
説明 |
|---|---|---|
|
GET |
ヘルスチェック ( |
|
POST |
|
|
GET |
保存されている名前を一覧表示する |
リクエスト/レスポンスの形状¶
POST /greetings
Content-Type: application/json
{ "name": "Ada" }
HTTP/1.1 201 Created
Content-Type: application/json
{ "ok": true, "name": "Ada" }
GET /greetings
HTTP/1.1 200 OK
Content-Type: application/json
{ "names": ["Ada", "Grace", "Linus"] }
ボディが欠落または不正な場合は 400 を返す:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{ "error": "Validation failed" }
npm スクリプト¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
KV セットアップの手順¶
同梱の wrangler.jsonc にはプレースホルダの ID が記載されており、置き換えるまで wrangler はそれを使用しない。README の KV Setup セクションでワークフロー全体を解説しているが、要点は次の通り:
# Production namespace (used by wrangler deploy)
npx wrangler kv namespace create GREETINGS
# Preview namespace (used by wrangler dev)
npx wrangler kv namespace create GREETINGS --preview
各コマンドは id を含む JSON スニペットを表示する。それらを wrangler.jsonc の kv_namespaces[0].id と .preview_id にそれぞれコピーする。配線は次のコマンドで確認できる:
npx wrangler kv namespace list
ローカルでの読み書きは既定で preview ネームスペースを経由する:
# Writes against preview_id (what wrangler dev sees)
npx wrangler kv key put --binding=GREETINGS hello world
# Writes against the production id explicitly
npx wrangler kv key put --binding=GREETINGS --remote hello world
preview_id を省略すると、wrangler はセッション間で消去される使い捨てのインメモリストアにフォールバックする。使い捨てデモには問題ないが、後で内容を確認したい場合には脆い。
バインディングを拡張する¶
同梱の Kv.res は例で使うルートをカバーしているが、Workers KV の表層全体を網羅しているわけではない。必要になったら直接バインディングを追加する — KV 操作のほとんどは 1 行の @send で書ける:
// Conditional writes (skip if the key already exists):
@send external putWithMeta:
(namespace, string, string, {"metadata": 'meta}) => promise<unit> = "put"
// Bulk delete:
@send external deleteKey: (namespace, string) => promise<unit> = "delete"
// Pagination — `list` accepts `{prefix, limit, cursor}`:
type listOpts = {prefix?: string, limit?: int, cursor?: string}
@send external listWithOpts:
(namespace, listOpts) => promise<listResult> = "list"
他の Workers プリミティブも同じ形に従う。それぞれが独自の薄いモジュールを持つ:
バインディング |
モジュール |
Wrangler 設定キー |
代表的なメソッド |
|---|---|---|---|
KV |
|
|
|
R2 |
|
|
|
Durable Objects |
|
|
|
D1 |
|
|
|
Queues |
|
|
|
いずれの場合も、wrangler.jsonc でバインディングを宣言し、Server.res の type env にフィールドを追加し、ルートハンドラ内で %raw("ctx.env") 経由で読み取る。パターンが統一されているため、サービスをまたいでマッスルメモリが活かせる。
試してみる¶
wrangler dev が起動し、KV ID が配線されたら (上記の KV Setup Walkthrough を参照)、サンプルをエンドツーエンドで動かしてみる:
# Store a greeting
curl -X POST http://localhost:8787/greetings \
-H 'Content-Type: application/json' \
-d '{"name":"Ada"}'
# => {"ok":true,"name":"Ada"}
# List the names you have stored
curl http://localhost:8787/greetings
# => {"names":["Ada"]}
# Send something the validator rejects
curl -X POST http://localhost:8787/greetings \
-H 'Content-Type: application/json' \
-d '{}'
# => 400 {"error":"Validation failed"}
Cloudflare へのプッシュ準備ができたら、npx wrangler login を一度実行し、その後 pnpm deploy (またはお使いの PM の同等コマンド) を実行する。最初のデプロイで <worker-name>.<account>.workers.dev にルートがプロビジョニングされ、以降のデプロイはアトミックで、ヘルスチェックに失敗した場合は自動でロールバックされる。
2日目以降のレシピ¶
Hono エンドポイントの追加 —
/greetingsと並行して別のルートを追加するTypeScript からの変換 — 既存の Worker をモジュールごとに ReScript へ移植する
Import の最適化 — バンドルサイズを削る (エッジでは 1 バイトが効いてくる)
プロジェクトを開いた後の ReScript 側のエディタワークフローについては、機能概要 を参照してほしい。
補足¶
Node ランタイムは使用しない。 Cloudflare Workers は Node ではなく V8 isolate で動作する。テンプレートが
@hono/node-server抜きでhonoのみを同梱し、エントリファイルの末尾が%%raw("export default app")になっているのはこのためである — Workers はデフォルトエクスポートのfetch(request, env, ctx)を呼び出す。.nvmrcが依然として24を指定しているのは、ツール群 (wrangler、内部の esbuild、vitest) がローカルでは Node 上で動作するためである。wrangler.tomlではなくwrangler.jsoncを採用。 Cloudflare は両方をサポートしている。JSONC を選んだ理由は、KV セットアップの解説を、説明対象のバインディングの隣に置けるためである — 別ファイルを見渡す必要がない。KV ネームスペースを 2 つ用意するのは意図的。
id(本番) とpreview_id(ローカル) を分けておくことで、wrangler devが本番に書き込んでしまうことがない。preview_idを省略すると、wrangler はセッション間で消去される使い捨てのインメモリストアにフォールバックする — 使い捨てデモには問題ないが、後で内容を確認したい場合には脆い。npx wrangler kv namespace create GREETINGSと... --previewを一度ずつ実行し、表示された ID を貼り付ける。バインディングの型は生成ではなく手書き。 テンプレートは
@cloudflare/workers-typesに依存していない。envの形状はServer.resにtype env = {"GREETINGS": Kv.namespace}として記述されている。バインディング (Durable Objects、R2、queues) を追加する場合は、その型にフィールドを追加し、wrangler.jsoncにバインディングエントリを追記する。.dev.varsは gitignore 済み。 wrangler が自動で読み取るローカル専用のシークレットを ここに置く。本番ではnpx wrangler secret put NAMEを実行する — シークレットをwrangler.jsoncに置いてはならない。.wrangler/とdist/は gitignore 済み。 前者には wrangler がローカル状態をキャッシュする。後者は将来追加するバンドル工程のために予約されている。スモークテストはモジュールのロードのみを検証する。
src/__tests__/Server.test.mjsはawait import("../Server.res.mjs")を実行し、それが解決することをアサートする。ルートにカバーする価値のあるロジックが入ってきたら、wranglerのunstable_devを使って KV バインドの統合テストを追加するとよい。デプロイには認証が必要。 マシンごとに
npx wrangler loginを一度実行し、その後pnpm deploy(またはお使いの PM の同等コマンド) でアカウントへプッシュする。README の Deploy セクションは、選択したパッケージマネージャに応じた正しいコマンドを表示する。compatibility_flagsなし、保守的なcompatibility_date。 同梱のwrangler.jsoncは2024-01-01を使用し、フラグは指定していない。新しい Workers ランタイムの挙動 (Node.js 互換シム、nodejs_compat、streams_enable_constructorsなど) が必要になった場合は日付を更新する — このフィールドは設計上 opt-in であり、Cloudflare がランタイムを更新しても古いデプロイが壊れないようになっている。KV 以外のバインディング。 Durable Objects、R2 バケット、キュー、D1 データベース、サービスバインディングを追加するには、
wrangler.jsoncを編集し、Server.resのtype envを拡張する。パターンは変わらない: JSONC でバインディングを宣言し、envレコードでその型を命名し、%raw("ctx.env")経由で読み取る。新しいバインディングごとに、Kv.resを模した薄い.resモジュールを用意する。無料プランの上限を意識する。 無料プランでは Worker のリクエストごとの CPU 時間予算は 50 ms である (2024 年 5 月以前は 10 ms) — バリデーション、KV 読み取り、JSON シリアライズもすべてここに含まれる。同梱のルートはこのエンベロープに余裕を持って収まっているが、重い暗号処理や大きな JSON を扱い始めたら、
wrangler dev --inspectを起動してプロファイルすること。