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 の name、ライセンス保有者、および wrangler.jsonc に展開される name フィールド (デプロイされる Worker 名) になる

Package manager

npm / pnpm / yarn / bun。packageManager フィールド、README の install/run コマンド、CI のキャッシュキーに影響する

Validation library

zodsurysrc/Validation.res を選択し、対応する依存を追加する。両者は parseGreetingPayload を公開するため、ルートハンドラは分岐する必要がない

主要な依存

パッケージ

用途

バージョン

rescript

ReScript コンパイラ

TemplateVersions.RESCRIPT

@rescript/core

標準ライブラリ

TemplateVersions.RESCRIPT_CORE

@rescript/runtime

コンパイル済み .res.mjs がインポートするランタイムスタブ

TemplateVersions.RESCRIPT_RUNTIME

hono

Workers ネイティブの fetch ハンドラを備えた HTTP ルーター

TemplateVersions.HONO

zod または sury

リクエストボディのバリデーション (ウィザードで選択)

TemplateVersions.ZOD / SURY

wrangler (dev)

Cloudflare CLI: 開発サーバー、KV、デプロイ

TemplateVersions.WRANGLER

vitest (dev)

スモークテストランナー

TemplateVersions.VITEST

@vitest/coverage-v8 (dev)

test:coverage 向けのカバレッジプロバイダ

TemplateVersions.VITEST_COVERAGE_V8

@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"
    }
  ]
}

idwrangler deploy (本番) が使用し、preview_idwrangler 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

ヘルスチェック (text/plain)

/greetings

POST

{ name } を検証し、KV に保存して 201 を返す

/greetings

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 スクリプト

スクリプト

説明

dev

wrangler dev — preview KV ネームスペースに対するローカル Workers ランタイム

deploy

wrangler deploy — Cloudflare にプッシュする

test

vitest run — スモークスイートを実行する

test:coverage

vitest run --coverage — 同じだが v8 カバレッジレポートを付与する

res:build

rescript — 単発のコンパイル

res:dev

rescript -w — 保存ごとに再コンパイル

res:clean

rescript clean — 生成された .res.mjs を削除する

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.jsonckv_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

Kv.res (同梱)

kv_namespaces

get / put / list / delete

R2

R2.res を追加

r2_buckets

put / get / list / head

Durable Objects

Do.res を追加

durable_objects.bindings

idFromName / get / fetch

D1

D1.res を追加

d1_databases

prepare / bind / all / first

Queues

Queue.res を追加

queues.producers / consumers

send / sendBatch

いずれの場合も、wrangler.jsonc でバインディングを宣言し、Server.restype 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日目以降のレシピ

プロジェクトを開いた後の 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.restype 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.mjsawait import("../Server.res.mjs") を実行し、それが解決することをアサートする。ルートにカバーする価値のあるロジックが入ってきたら、wranglerunstable_dev を使って KV バインドの統合テストを追加するとよい。

  • デプロイには認証が必要。 マシンごとに npx wrangler login を一度実行し、その後 pnpm deploy (またはお使いの PM の同等コマンド) でアカウントへプッシュする。README の Deploy セクションは、選択したパッケージマネージャに応じた正しいコマンドを表示する。

  • compatibility_flags なし、保守的な compatibility_date 同梱の wrangler.jsonc2024-01-01 を使用し、フラグは指定していない。新しい Workers ランタイムの挙動 (Node.js 互換シム、nodejs_compatstreams_enable_constructors など) が必要になった場合は日付を更新する — このフィールドは設計上 opt-in であり、Cloudflare がランタイムを更新しても古いデプロイが壊れないようになっている。

  • KV 以外のバインディング。 Durable Objects、R2 バケット、キュー、D1 データベース、サービスバインディングを追加するには、wrangler.jsonc を編集し、Server.restype env を拡張する。パターンは変わらない: JSONC でバインディングを宣言し、env レコードでその型を命名し、%raw("ctx.env") 経由で読み取る。新しいバインディングごとに、Kv.res を模した薄い .res モジュールを用意する。

  • 無料プランの上限を意識する。 無料プランでは Worker のリクエストごとの CPU 時間予算は 50 ms である (2024 年 5 月以前は 10 ms) — バリデーション、KV 読み取り、JSON シリアライズもすべてここに含まれる。同梱のルートはこのエンベロープに余裕を持って収まっているが、重い暗号処理や大きな JSON を扱い始めたら、wrangler dev --inspect を起動してプロファイルすること。