Cloudflare Pages Functions で API を 1 ファイル化する最小構成

Cloudflare Pages で静的サイトを公開すると、しばらくは「サーバーは要らない」状態で個人開発の本番運用が回ります。けれど、ある日 「フォームを受け取りたい」「Webhook を 1 本受けたい」「外部 API キーを隠して中継したい」 という用途が顔を出します。

このとき、Workers 単独プロジェクトを立てる のが本筋なのですが、サイトに密接した 1 つの API(同じドメインの /api/xxx)のために別プロジェクトを管理するのは、少し重い。デプロイも別、ドメイン設定も別、ローカル開発の起動コマンドも 2 つ。

Pages Functions は、このギャップを埋めるために用意された仕組みです。Pages プロジェクトのリポジトリの中に functions/ ディレクトリを 1 つ作り、functions/api/hello.ts のようにファイルを置くだけで、https://<site>/api/hello で動く Worker が一緒にデプロイされます。本記事では、その最小構成と、Workers 単独との使い分けを、一次情報で通します。

Pages Functions とは(Workers 単独との違い)

両者は同じ Workers ランタイム(workerd)の上で動きますが、どこに住んでいて、誰がデプロイするかが違います。

Pages FunctionsWorkers 単独
住所サイトと同じドメインの /<path>別ドメイン(<name>.workers.dev または custom)
デプロイサイトと一緒に git push(Pages の自動ビルド)wrangler deploy または別の Git 連携
設定ファイルwrangler.toml 不要・functions/ の構造で決まるwrangler.toml 必須
向く用途サイトに密接した 1〜5 個の API独立サービス・複数フロントから叩く API
ローカル起動wrangler pages devwrangler dev
bindings(D1 / R2 / KV)◎(dashboard でバケット選択)◎(wrangler.toml で書く)

サイトの中に住んでいる、サイトと同じ URL の API」── これが Pages Functions のキャラクターです。「独立したサービスとして外から叩かれる API」なら、迷わず Workers 単独で。

novelarena.jp のような Astro 静的サイト + たまに動的処理 の構成では、Pages Functions が最短の正解です。

手順 1:functions/ ディレクトリを作る

Pages プロジェクトのリポジトリのルートに、functions/ ディレクトリを作ります。

my-site/
├── src/             ← Astro / Next.js などのソース
├── public/
├── functions/       ← ★ ここを新設
│   └── api/
│       └── hello.ts
├── package.json
└── astro.config.mjs

ファイル名と階層がそのまま URL になります。functions/api/hello.tshttps://<site>/api/hello。Pages ビルド時に Cloudflare 側が自動で検知し、Worker として一緒にデプロイします。

手順 2:onRequest ハンドラを書く

functions/api/hello.ts の中身は、これだけです。

interface Env {}

export const onRequest: PagesFunction<Env> = async (context) => {
  const { request, env } = context;
  const url = new URL(request.url);
  const name = url.searchParams.get('name') ?? 'world';
  return Response.json({ message: `Hello, ${name}` });
};

Workers 単独は export default { fetch } でしたが、Pages Functions は export const onRequest という名前で関数を出します。中身は同じ Workers ランタイムなので、Request / Response / fetch が使えます。

HTTP メソッドごとに分けたい場合は、onRequestGet / onRequestPost / onRequestPut の名前でエクスポートします。

export const onRequestGet: PagesFunction<Env> = async () => {
  return Response.json({ ok: true });
};

export const onRequestPost: PagesFunction<Env> = async ({ request }) => {
  const body = await request.json();
  // ...
  return Response.json({ received: body });
};

これだけで、メソッドルーティングが完成します。onRequest 1 つで書いて自分で request.method を見る方式と、メソッドごとにエクスポートする方式、どちらでも動きます。

手順 3:パスパラメータを使う

functions/api/users/[id].ts のようにブラケットで囲んだファイル名にすると、URL のパス部分をパラメータとして受け取れます

// functions/api/users/[id].ts
export const onRequestGet: PagesFunction = async ({ params }) => {
  const id = params.id;  // /api/users/42 の "42" が入る
  return Response.json({ id });
};

複数階層のキャッチオールには [[path]].tsダブルブラケット)。Next.js を触ったことがある人なら、ほぼ同じ感覚です。

手順 4:ローカルで wrangler pages dev

ローカルで起動するには、wrangler pages dev を使います。

# Astro なら一度 build してから(Pages は dist/ を見る)
npm run build
npx wrangler pages dev ./dist

http://localhost:8788 が立ち上がります(Workers 単独の 8787 とは別ポート)。functions/ の変更は自動で拾われます。

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

静的ファイル(dist/index.html)も、API(/api/hello)も、同じドメイン・同じポートで配信される ── これが Pages Functions の心臓部です。

手順 5:デプロイは git push だけ

Pages はすでに Git 連携でデプロイが回っている前提です。functions/ の追加と削除も、git push に乗ってそのまま反映されますwrangler deploy を別途叩く必要はありません。

git add functions/
git commit -m "feat: /api/hello を追加"
git push origin main

数十秒〜数分で Pages のビルドが回り、本番に反映されます。https://<your-site>/api/hello が動き始める。

環境変数と bindings

API キーや、D1 / R2 / KV を Pages Functions の中で使いたい場合は、Cloudflare ダッシュ → Pages → 該当プロジェクト → Settings → Functions から設定します。

  • 環境変数(平文)Environment variables セクションで Name / Value で追加。context.env.MY_VAR で参照。
  • シークレット(機密):同じ画面で Encrypt を ON にして追加。コード上は同じく context.env で読める。
  • D1 / R2 / KV bindingsD1 database bindings / R2 bucket bindings / KV namespace bindings セクションで、すでに作ったバケットや DB を選択。

ダッシュから設定するのは、Pages Functions が wrangler.toml を持たないため。Workers 単独だと TOML に書きますが、Pages Functions ではダッシュが正本です。

ローカル開発時の bindings は、.dev.vars ファイル(プロジェクトルート)に書きます。

# .dev.vars(Git に入れない)
MY_API_KEY=xxxxx

D1 / R2 / KV のローカル bindings は、wrangler pages dev--d1 DB=<db-name> のように引数で渡せます(オプションが増えるので、頻繁に使うなら npm script に固定しておくのが楽)。

挫折ポイント

  • ファイル名と階層がそのまま URLfunctions/api/hello.ts/api/hellofunctions/api/users/[id].ts/api/users/<id>サイトの静的パス(例:/api という静的ページ)と重なると、Pages Functions のほうが優先されてしまうので、/api/* 配下は API 専用にする習慣。
  • onRequestonRequestGet:両方書くと onRequest が優先されます。メソッド別の挙動を書きたいときは onRequest 系を消して onRequestGet / onRequestPost に揃える。
  • wrangler.toml を書きたくなる:Pages Functions は wrangler.toml を読みません(Workers 単独との大きな違い)。ダッシュ設定が正本。コードと一緒に env を版管理したい場合は、その API を Workers 単独に切り出すサインです。
  • functions/ の中の TypeScript 型PagesFunction<Env> を読み込むには、@cloudflare/workers-types を入れて tsconfig.jsontypes に追加します。型なしでも動きますが、補完が効くと書き心地が変わる。
  • 静的ファイルが Functions で上書きされる気がする:Pages は「まず Functions、なければ静的」の順で見ます。サイトの静的ファイル名と URL がぶつからない限り、両立できます。
  • ローカル wrangler pages dev が遅い:build を毎回回すと重いので、開発中は Astro / Next.js の dev server を別途立てて、API だけ別ポートで叩く割り切りも有効。本番では同じドメインに統合されるので、本番テストは別途必要。

まとめ:5 ステップで「サイト + 小さな API」が同居する

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

  1. プロジェクトルートに functions/api/hello.ts を 1 つ作る
  2. export const onRequest = async (context) => Response.json(...) を書く
  3. パスパラメータは [id].ts、キャッチオールは [[path]].ts
  4. ローカル確認は wrangler pages dev ./dist
  5. デプロイは普段の git push origin main(Pages のビルドに自動で乗る)

ここまで通れば、**「静的サイトの中に、ちょっとだけ動的処理が住んでいる」**構造が手に入ります。フォーム受け、Webhook、外部 API キーの中継、簡単な認証リレー ── どれも functions/api/*.ts の延長で書けます。

そして、「いや、これは独立サービスだ」と感じ始めたら、Workers 単独に切り出すタイミングです。Pages Functions と Workers 単独の境目は、最初から決め打ちしなくても、書き進める中で見えてきます。だいたいの目安は、「API の数が 5 〜 10 を超え始めたら」「複数フロントから叩かれるようになったら」「wrangler.toml を書きたくなったら」── そのどれかが立ったら、別プロジェクトに引っ越します。

姉妹記事として、静的サイトの公開先は Cloudflare Pages、独立 API は Cloudflare Workers、データ層は D1、ファイル層は R2 を書いています。Pages Functions を含めて、Cloudflare 1 アカウントで個人開発のフルスタックが完結する構成、として並べて読めます。

最初の 1 ファイルを、まず functions/api/hello.ts に置くところから。


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

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