Cloudflare Workers で最小の API を公開する手順

静的サイトを Cloudflare Pages に置いたあと、次に欲しくなるのは 「サーバー側で 1 関数だけ動かしたい」 という用途です。フォームの送信を受けて Discord に流す、外部 API のキーを隠したまま中継する、Webhook を受ける、価格データを取得してキャッシュする ── どれも「サイトのなかに置いておきたいが、ブラウザに直に書きたくない」処理です。

このとき登場するのが Cloudflare Workers。サーバーレスのファンクション基盤で、世界中のエッジで動き、無料枠だけでも個人サービスはほぼ完結します。本記事では、その Workers に 最小の API を 1 本デプロイするまで を、wrangler CLI の initdevdeploy の 3 ステップで通します。

確認は wrangler v3 系・Node 22 を一次情報にしています。先に静的サイトの公開先を整理した 個人開発者のサイトを Cloudflare Pages で公開する手順 と対で読むと、Pages(静的)と Workers(動的)の棲み分けが立体的に入ります。

なぜ Workers なのか(Pages との違い)

最初に、Pages と Workers の役割をはっきりさせます。同じ「Cloudflare 上のサーバーレス」でも、見ている層が違います。

Cloudflare PagesCloudflare Workers
主な用途静的サイト配信(HTML / CSS / JS)サーバー処理(fetch ハンドラを書いて返す)
デプロイgit push → 自動ビルドwrangler deploy(または Git 連携)
実行モデルファイルを CDN に置くリクエストごとに JS が実行される
課金単位ビルド回数リクエスト回数+ CPU 時間
無料枠(2026 年現在の感覚値)500 ビルド/月・帯域無制限10 万リクエスト/日・10ms CPU/req
同居の DB / KVなし(別途 Workers + D1 等)同じ Worker から env.DB 等で参照

Pages のページ内で /api/xxx だけ動かしたい」という用途には、Pages 上で動く Pages Functions という選択肢もあります。ファイルベースで /functions/api/xxx.ts を置くだけで API 化される、Pages 専用の薄い Worker です。ただ、本格的に「API を独立 URL で運用する」ならば、最初から Workers 単独プロジェクト で立ち上げるほうが、後で混乱しません。本記事は Workers 単独の経路で進めます。

前提:Node と wrangler

ローカルで以下が成立している前提です。

  • Node 18 以上(推奨 20 / 22)。.nvmrc で固定済みなら何もしない。
  • Cloudflare アカウント(無料)。

wrangler 自体は、プロジェクトを作るときに npm が自動で入れます。グローバルにインストールする必要はありません(むしろバージョン差で詰むので、プロジェクトごとに devDependencies に入るほうが安全)。

ログインだけ先に通します。

npx wrangler login

ブラウザが開いて、Cloudflare の OAuth 画面で認可します。wrangler whoami でアカウントが出れば OK です。

手順 1:wrangler init で雛形を作る

新しい空ディレクトリで、次の 1 行から始めます。

mkdir my-api && cd my-api
npm create cloudflare@latest .

対話で聞かれます。最小構成なら以下で OK。

  • What would you like to start with?Hello World example
  • Which template would you like to use?Worker only(Pages との同居が要らないなら)
  • Which language do you want to use?TypeScript(型補完が効くので推奨)
  • Do you want to use git for version control?Yes
  • Do you want to deploy your application?No(最初は手動で確認する)

完了すると、以下のような最小構成が生成されます。

my-api/
├── src/
│   └── index.ts          ← fetch ハンドラはここ
├── wrangler.toml         ← 設定ファイル
├── package.json
└── tsconfig.json

src/index.ts を開くと、これだけです。

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    return new Response('Hello World!');
  },
};

export default { fetch }Worker のすべて。リクエストが来ると、この fetch が呼ばれて、返した Response が世界中のエッジから返る ── これが Workers の心臓部です。

wrangler.toml のほうは、こんな最小です。

name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-01-01"
  • name:そのまま https://my-api.<your-subdomain>.workers.devmy-api 部分になります。
  • compatibility_date:Workers ランタイムのバージョン固定。生成された日付のまま で問題ありません(古いまま放置せず、年に 1 回ほど見直す程度)。

手順 2:ローカルで wrangler dev を動かす

デプロイ前に、ローカルで動かして確認します。

npx wrangler dev

http://localhost:8787 が立ち上がります。curl http://localhost:8787 を叩くと Hello World! が返ってくる。これでローカル動作 OK です。

wrangler dev の便利なところは、実際の Workers ランタイム(workerd)でローカル実行する こと。Node の http モジュールで動いているわけではなく、本番と同じ runtime(Request / Response / fetch グローバル)で動くため、「ローカルでは動いたのに本番で動かない」が起きにくい。

ファイルを編集すれば自動でリロードします。src/index.ts を以下のように書き換えて、簡単な JSON API にしてみます。

interface Env {}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === '/api/hello') {
      const name = url.searchParams.get('name') ?? 'world';
      return Response.json({ message: `Hello, ${name}` });
    }

    return new Response('Not Found', { status: 404 });
  },
};

curl 'http://localhost:8787/api/hello?name=mahiro'{"message":"Hello, mahiro"} が返れば OK。

手順 3:wrangler deploy で本番公開

ローカルで動いたら、デプロイは 1 行です。

npx wrangler deploy

数秒で本番 URL が払い出されます。

Published my-api (1.23 sec)
  https://my-api.<your-subdomain>.workers.dev
Current Deployment ID: ...

その場で curl https://my-api.<your-subdomain>.workers.dev/api/hello?name=mahiro を叩くと、エッジ越しに同じ JSON が返ります。世界中の Cloudflare エッジに、コードが配信完了している状態です。

環境変数とシークレット

外部 API キーや署名鍵を Worker から使う場面が出てきます。これには 2 種類あります。

平文の環境変数(公開して構わない値)

wrangler.toml に書きます。

[vars]
APP_NAME = "novelarena-api"
LOG_LEVEL = "info"

コード側からは env.APP_NAME で参照。wrangler.tomlそのまま Git に入れる前提 なので、ここに秘密は書かないこと。

シークレット(API キー等の機密)

専用コマンドで登録します。

npx wrangler secret put OPENAI_API_KEY
# プロンプトが出るので値を貼り付け、Enter

これは Cloudflare 側に暗号化保存され、コード上からは同じく env.OPENAI_API_KEY で読めます。ローカル .dev.vars ファイルに同名で書いておくと、wrangler dev のローカル実行時にだけ読まれます.dev.vars.gitignore 必須)。

# .dev.vars(Git に入れない)
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx

シークレットの値を画面に出さないのは、ターミナルの履歴に残さないため。wrangler secret list で名前は確認できますが、値は見られない設計です。秘密はコードにもログにも出さない のが基本です(GA4 イベント計測の挫折ポイントで書いた「PII を計測パラメータに入れない」と同じ原則)。

カスタムドメインを当てる

workers.dev のサブドメインのままでも動きますが、api.example.com のような自分のドメインで運用したくなります。やり方は 2 つ。

  • Custom Domain(推奨):Workers ダッシュ → 該当 Worker →「Triggers」→「Custom Domains」→「Add Custom Domain」で api.example.com を入力。ドメインが Cloudflare のゾーン上にあれば、DNS と SSL は自動で当たります。
  • Routeexample.com/api/* のように、既存サイトのパスにだけ Worker を当てたい場合。wrangler.tomlroutes を書くか、ダッシュから設定。Pages と同居する場合に使いがちですが、最初は Custom Domain のほうがシンプルです。

観測:wrangler tail でログを流す

本番で動き始めると、最初のうちは「動いているのか」「失敗しているのか」を見たくなります。

npx wrangler tail

これで、本番 Worker の console.log と例外がリアルタイムで流れて来ます。コード側で console.log(JSON.stringify({ path, status, ms })) 等を仕込んでおくと、tail でそのまま読めます。

長期的なログ収集は Workers Logs(CF ダッシュの該当 Worker → Logs)で永続化されますが、開発中の体感は wrangler tail のほうがずっと速い。

挫折ポイント

  • 無料枠の上限を意識する:1 日 10 万リクエスト・1 リクエストあたり CPU 10ms。個人の API なら十分ですが、外部から無差別に叩かれるエンドポイントだと、Bot Fight ModeRate Limiting Rules(CF 側機能)で守る。
  • wrangler devworkerd ランタイムで動く意味:Node の API(fsprocess 等)は 使えませんcrypto.subtlefetchRequestResponseURL は使える。ライブラリを npm install するときも、Workers 対応のものを選ぶ(package.json の engines や README に明記されています)。
  • bundler のエラーwrangler 内蔵の esbuild で src/index.ts から依存をたどってバンドルします。動的 import(require(variable))は壊れる。ESM 形式に揃える。
  • CORS が要る場面:別ドメインのフロント(Pages の novelarena.jp)から叩くなら、ResponseAccess-Control-Allow-Origin 等のヘッダを付ける。プリフライト(OPTIONS)も自分でハンドルする必要があります。
  • シークレットを wrangler.toml に書いてしまった:履歴に残るので、即 git rm --cached wrangler.toml(再追加前に書き直し)+ キーをローテート。これは Workers に限らず、すべての設定ファイル共通の鉄則です。
  • compatibility_date が古すぎて新 API が使えない:1 年以上前の日付を放置していると、新しい標準 API(例えば新しい Request の機能)が動かないことがあります。エラーメッセージで気づいたら更新します。
  • workers.dev サブドメインが Google にインデックスされる:個人 API では実害は少ないですが、嫌なら robots.txt を Worker 自身が返すか、CF ダッシュからサブドメインを disable できます。

まとめ:Worker は「fetch ハンドラを 1 個書いて deploy する」だけ

最小手順を振り返ります。

  1. npm create cloudflare@latest .(Hello World テンプレで Worker only
  2. src/index.tsexport default { fetch } を書き換える
  3. npx wrangler dev でローカル確認
  4. npx wrangler deploy で本番公開
  5. シークレットは wrangler secret put、独自ドメインは Triggers → Custom Domains

ここまで通れば、もう「サーバーがほしい」と思った瞬間に、5 分でエンドポイントが立ちます。フォーム送信の中継、Webhook 受け、外部 API のキャッシュ層、Discord / Slack 通知、認証のリレー ── どれも fetch ハンドラ 1 個の延長で書けます。

Workers の次の段で気になるのが、データを持ちたい という欲求です。これには Cloudflare 側に D1(SQLite 系のサーバーレス DB)、KV(Key-Value)、R2(オブジェクトストレージ)が同居する形で用意されていて、Worker から env.DB / env.MY_KV / env.MY_BUCKET のように bindings で参照する設計になっています。「Worker 1 関数 + KV 1 つ」「Worker 1 関数 + D1 1 個」 の組み合わせで、個人開発のバックエンドの 9 割が成立します。

公開した API を本番で動かし始めたら、対のサイト側でも どの呼び出しが何回起きているか を見たくなります。サイト側の計測は 個人開発者が GA4 で最初に設定する 3 つのイベントGA4 で最初に見る 3 つの数字 で扱いました。Worker 側の観測(wrangler tail と Logs)と合わせて、フロントの動き ↔ バックエンドの実行 が両側から見える状態を作っておくと、後で「なぜ動いていないか」を掘る入り口が増えます。

そして、API が動き始めて呼び出し数が増えても、ユーザーの登録は伸びない ── そう感じたとき、原因は API でも UI でもなく、サービスの 最初の一行 にあります。その構造を掘ったのが 個人開発者のサービスが「素通り」される、たったひとつの構造的な理由 と、作る側の誤算を扱った 「プロダクトは語る」と思っていた個人開発者が陥る 3 つの誤算 です。サーバーが動いてもプロダクトが伝わらない、その裏側を続けてどうぞ。

最初の 1 関数を、まず wrangler deploy するところから。


サーバーは動かせる、サイトも公開できる。けれど 読まれない/登録に至らない ── その構造を WHY → HOW で 1 冊にまとめたのが、下記の本です。個人開発者・インディー Web 作者・動画作者向けに、LP・短文・メールの戦場別の書き方と、Claude を相棒にしたプロンプト連鎖の実演を含めて統合しています。

#個人開発#Cloudflare#リファレンス