Cloudflare Workers で最小の API を公開する手順
静的サイトを Cloudflare Pages に置いたあと、次に欲しくなるのは 「サーバー側で 1 関数だけ動かしたい」 という用途です。フォームの送信を受けて Discord に流す、外部 API のキーを隠したまま中継する、Webhook を受ける、価格データを取得してキャッシュする ── どれも「サイトのなかに置いておきたいが、ブラウザに直に書きたくない」処理です。
このとき登場するのが Cloudflare Workers。サーバーレスのファンクション基盤で、世界中のエッジで動き、無料枠だけでも個人サービスはほぼ完結します。本記事では、その Workers に 最小の API を 1 本デプロイするまで を、wrangler CLI の init → dev → deploy の 3 ステップで通します。
確認は wrangler v3 系・Node 22 を一次情報にしています。先に静的サイトの公開先を整理した 個人開発者のサイトを Cloudflare Pages で公開する手順 と対で読むと、Pages(静的)と Workers(動的)の棲み分けが立体的に入ります。
なぜ Workers なのか(Pages との違い)
最初に、Pages と Workers の役割をはっきりさせます。同じ「Cloudflare 上のサーバーレス」でも、見ている層が違います。
| Cloudflare Pages | Cloudflare 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.devのmy-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 は自動で当たります。 - Route:
example.com/api/*のように、既存サイトのパスにだけ Worker を当てたい場合。wrangler.tomlにroutesを書くか、ダッシュから設定。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 ModeやRate Limiting Rules(CF 側機能)で守る。 wrangler devがworkerdランタイムで動く意味:Node の API(fs、process等)は 使えません。crypto.subtle、fetch、Request、Response、URLは使える。ライブラリをnpm installするときも、Workers 対応のものを選ぶ(package.json のenginesや README に明記されています)。- bundler のエラー:
wrangler内蔵の esbuild でsrc/index.tsから依存をたどってバンドルします。動的 import(require(variable))は壊れる。ESM 形式に揃える。 - CORS が要る場面:別ドメインのフロント(Pages の
novelarena.jp)から叩くなら、ResponseにAccess-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 する」だけ
最小手順を振り返ります。
npm create cloudflare@latest .(Hello World テンプレでWorker only)src/index.tsのexport default { fetch }を書き換えるnpx wrangler devでローカル確認npx wrangler deployで本番公開- シークレットは
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 を相棒にしたプロンプト連鎖の実演を含めて統合しています。