システム開発
約20分
更新

問い合わせフォームをCloudflare Workersで実装する:Turnstile・Resend連携

執筆・編集Malake株式会社
まず押さえたいこと

Cloudflare Workersとは、Cloudflareのネットワーク上でコードを実行するサーバーレス環境です。静的ファイルの配信だけでは、メール送信のようなサーバー側の処理は実行されません。フォームの受信処理をWorkersやPages Functionsに受け持たせると、専用のサーバーを常時稼働させずに実装できます。

この記事の要点

静的サイトのフォームは、ブラウザからメールAPIを直接呼ばず、WorkersやPages Functionsなどのサーバー側処理で入力検証、トークンの検証、送信、記録を行います。Turnstileは対策の一つであり、レート制限、失敗時の表示と通知、ログの保存期間も合わせて設計します。メールAPIの受付と受信側への配信は分けて確認し、採用するサービスの料金と上限は導入時に確認します。

この記事でわかること

  • なぜ Pages だけでは“問い合わせフォーム”が難所なのか
  • この記事の前提:Workersで実装する
  • アーキテクチャ(Bot排除とメール送信)
  • 実装手順(番号付きガイド)
  • 運用設計(脅威モデルと対策)
  • 公開前に確認する送信・障害時の動作
  • Vercel と比較(開発者目線)
  • 発展:WAF / CDN / Zero Trust への布石
  • まとめ
  • よくある質問 (FAQ)
  • 参考リンク / 出典

なぜ Pages だけでは“問い合わせフォーム”が難所なのか

Cloudflare Pagesに置いたHTMLやCSSなどの静的ファイルは、そのまま配信されます。静的ファイルの配信だけでは、「メールを送る」「データベースに保存する」といったサーバー側の処理は実行されません。PagesではPages Functionsでサーバー側のコードを動かせるため、フォームの受信処理はPages Functions、別のWorker、外部のフォームサービスのいずれかで補います。

ブラウザのJavaScriptからメールAPIを直接呼ぶと、APIキーを公開することになり、第三者に送信機能を悪用されるおそれがあります。キーを扱う処理はサーバー側に置きます。

補足:この記事は、実装と運用を中心にした技術的な解説です。静的配信で減らせる維持費と残る費用(経営者・Web担当者向け)は、ホームページ維持費の記事で扱っています。

表は横にスクロールできます →

▼ フォーム実装の3つの選択肢
方式A. 外部のフォームサービスB. Pages FunctionsC. 別のWorker
概要フォームサービスの埋め込みやリンクで受け付けるPagesプロジェクトの中に受信処理を置き、サイトと一緒に配備する受信処理を独立したWorkerとして公開する
選びやすい条件画面・項目がサービスの範囲で足り、データをサービス側に保存してよい既存のPagesサイトに受信処理を加え、同じプロジェクトで管理したいサイトと受信処理を別々に公開・運用したい。複数のサイトや画面から同じ処理を使う
確認すること見た目と項目の自由度、データの保存先と取り扱い、通知の方法、料金秘密値の設定、ログの確認方法、サイトの配備と一緒に変わる範囲呼び出し元のオリジン(CORS)、秘密値の設定、ログの確認方法
運用の分担サービスの設定と契約を管理する担当サイトとフォームの処理を同じ担当・同じ手順で更新するサイトとは別に、配備の手順と担当を決める

どの方式でも、入力の検証、スパム対策、通知先、失敗したときの確認は必要です。この記事では、サイトと受信処理を独立して公開・運用したい場合の構成例として、Cの別のWorkerで手順を示します。

この記事の前提:Workersで実装する

この記事は、フォームの受信処理を独立したCloudflare Workerで実装する構成例です。サイトと受信処理を別々に公開・運用したい場合を想定しています。秘密値の保存やログの確認はPages Functionsでも使えるため(Pages FunctionsのSecrets・ログ)、それだけでWorkersを選ぶ理由にはなりません。

Cloudflareは、新しいプロジェクトをWorkersで始めるよう案内しています(Cloudflare Pagesの概要)。新しく作る場合はWorkersを第一の候補として検討し、既存のPagesサイトでは、Pages Functionsで受信処理を加えて運用を続ける方法もあります。構成全体の選び方はCloudflare PagesとWorkersの違い:構成を選ぶための判断表で扱っています。Workers側の認証・キャッシュ・運用の設計はCloudflare Workersの実装設計を参照してください。

アーキテクチャ(Bot排除とメール送信)

今回の構成例の全体像です。ブラウザからは直接メールを送らず、Workerで受け付けて検証してから、メールAPIを呼び出します。

Turnstileのトークンは、Workerがサーバー側の検証(Siteverify)で確認します。検証に失敗した場合はメールAPIへ進まず、メールAPIがエラーを返した場合も送信済みとして扱いません。

Loading Diagram...

メールAPIが送信を受け付けた(送信IDが返った)ことは、受信側への配信や受信箱での確認とは別です。配信状態の確認は「運用設計」で扱います。

実装手順(番号付きガイド)

構築時に確認する手順を、7つの段階で示します。

サイトと受信処理が別のオリジン(ドメインなど)にある場合は、Worker側でCORSを設定し、許可するオリジンを指定します。CORSはブラウザが適用する仕組みのため(MDN:CORS)、利用者の認証の代わりにはならず、ブラウザ以外からの直接の呼び出しも防げません。

自社の状況を確認する

  • □ TurnstileのSecret KeyとResendのAPI Keyを取り違えていないか
  • □ 秘密値が本番の環境に設定され、Workerから読み込めているか
  • □ 別のオリジンから呼ぶ場合、CORSで自社サイトのオリジンだけを許可しているか

Cloudflare Turnstile の発行

Cloudflareダッシュボード > Turnstile でウィジェットを追加します。
フォームを置くドメイン名を登録し、ウィジェットのモードを選びます。
「Site Key(公開)」と「Secret Key(非公開)」の2つを取得します。Secret Keyはサーバー側だけで使います。

Resend のAPI Key発行とドメイン設定

ResendでAPI Keyを作成します。
[Domains] で自社が管理するドメイン(またはサブドメイン)を追加し、表示されるDNSレコードを登録して検証を完了します。Resendの送信には検証済みのドメインが必要です。
送信元(From)は検証したドメインのアドレスにします。利用者が入力したアドレスはFromに入れず、返信先が必要な場合は検証した入力をReply-Toに設定します。通知先はサーバー側で決め、フォームから指定させません。
SPF・DKIM・DMARCなどの認証は受信側の判定材料の一つで、これだけで迷惑メール扱いを完全に防げるわけではありません。

Workerプロジェクトの作成

ターミナルで `npm create cloudflare@latest contact-api` を実行します。
テンプレートは「Hello World」などの最小構成を選びます。

Secrets(環境変数)の設定

キーはコードに直接書かず、秘密値として保存します。Workersではwranglerコマンドで設定できます。
実行:`npx wrangler secret put TURNSTILE_SECRET_KEY`
実行:`npx wrangler secret put RESEND_API_KEY`

Workerのコードの実装(入力の検証と送信)

次の順序で処理します。条件を満たさない場合は、メールAPIを呼ばずにエラーを返します。
1. 受付条件の確認(メソッド、データの形式、本文のサイズ)
2. 入力値の検証(必須項目、文字数、メールアドレスの形式など)
3. Turnstileトークンのサーバー側検証(Siteverify)。成功に加え、必要に応じてhostnameやactionが想定どおりかを照合
4. メールAPIの呼び出し(送信元と通知先はサーバー側で設定)
5. 戻り値とエラーの判定(429・5xx・タイムアウトは成功として扱わない)
6. 利用者への応答と、問い合わせID・処理結果・時刻の記録

クライアント側(Pages)の実装

Turnstileのウィジェットを表示し、フォーム送信時にトークンを取得します(`react-turnstile` などのライブラリを使う方法もあります)。
取得したトークンを、Workerへ送るPOSTの本文に含めます。
トークンは発行から300秒で失効し、1回しか検証できません。検証に失敗した場合や送信をやり直す場合は、ウィジェットをリセットして新しいトークンを取得します。

デプロイと動作確認

`npx wrangler deploy` で公開します。
公開前に、正常な送信と、入力の誤り・トークンの失効・メールAPIのエラーなどの異常時の動作を確認します(確認する場面は「運用設計」の表を参照)。
メールAPIの受付(送信ID)、Resendでの配信状態、通知先での受信を分けて確認します。
← 横にスクロールできます →

運用設計(脅威モデルと対策)

公開したフォームは、ボットによる送信や、送信機能の悪用の対象になります。主な脅威と対策を整理します。1つの対策だけで防げるものではないため、適用範囲を確認して組み合わせます。

表は横にスクロールできます →

▼ 主な脅威と対策
脅威具体的な攻撃内容主な対策と確認すること
ボットによるスパム広告や詐欺のURLを含む問い合わせを大量に送るTurnstileのサーバー側検証、入力値の検証、隠しフィールド(ハニーポット)。内容による制限も組み合わせる
大量送信・DoSAPIを過剰に呼び出し、メール送信の上限や費用を消費させる多重送信の防止、レート制限(WAFのルールやWorker内の制限)。WAFのルールは対象のホスト・パスと契約プランで使える条件が異なる。通常の利用者を誤って遮断しないかを確認する
なりすまし自社のドメインをかたったメールを送るSPF・DKIM・DMARCの設定、Resendで検証済みのドメインだけを使う。利用者の入力をFromに使わない
キーの漏えいAPI Keyをリポジトリにコミットし、悪用される秘密値として管理し、コードに書かない。漏れた場合に無効化・再発行する手順を決める

Turnstileは、訪問者に画像選択などのパズルを表示せずに判定できる仕組みです(Turnstile)。判定の結果はサーバー側の検証で確認する必要があり、トークンは発行から300秒で失効し、1回しか検証できません(Siteverify)。検証のAPIに接続できない場合も、通過させずにエラーとして扱います。Turnstileだけで大量送信や不正な入力を防げるわけではないため、対策の適用範囲、レート制限、通常の利用者への影響、通知とログの確認を組み合わせます。

メールAPIが429・5xxを返した場合やタイムアウトした場合は、利用者に失敗または再試行の案内を表示し、受付済みかどうか分からない送信は照合します。同じ問い合わせの重複を防ぐには、問い合わせごとにIDを決め、それに対応するメール送信用の重複防止キー(ResendのIdempotency-Key)を、再試行でも同じ値で使います。再試行のたびに別のキーを発行すると、重複の防止になりません。Resendはこのキーを24時間保持します(Resend:Idempotency Keys、確認日:2026年10月22日)。Turnstileのトークンは人による操作かを確かめるためのもので、1回しか検証できないため、問い合わせの識別や重複の防止には使いません。

公開前に確認する送信・障害時の動作

公開前に、正常な送信に加えて、失敗したときの画面・記録・担当を確認します。次の表は、確認する場面の例です。

表は横にスクロールできます →

▼ 公開前に確認する場面
確認する場面期待する動作確認する記録と担当
正常な入力フォームの受付とメールAPIの受付を分けて記録し、両方が成功してから完了を表示する問い合わせIDと送信IDを照合できる記録。照合する担当
不正な入力・受付条件の不一致処理を拒否してメールを送らず、利用者が修正できる案内を表示する拒否した理由の区分と時刻
トークンの失効・再使用・検証の失敗通過させずメールを送らない。必要に応じて新しいトークンの取得を案内する検証結果の区分と時刻
メールAPIの429・5xx成功と表示せず、時間をおいた再送や別の連絡方法を案内する。再試行する条件を決めておくAPIの応答と時刻。担当への通知
メールAPIのタイムアウト受付済みかどうかを照合し、同じ問い合わせを条件なしに再送して重複させない問い合わせIDと重複防止のキー。照合する担当
配信の遅延・バウンスメールAPIの受付とは分けて配信状態を確認する。受信側のメールサーバーへの配信も、受信箱での表示や閲覧を保証するものではないResendの配信イベントや送信ログの確認先と、確認する担当
連打・大量送信多重送信の防止とレート制限が働き、通常の利用者が誤って遮断されない制限した件数と時刻。誤って遮断された場合の連絡を受ける担当

記録には、問い合わせの本文や秘密値をそのまま残さないようにします。照合に必要なID、処理結果、時刻などに絞り、確認する担当と保存期間を決めます。ログが保存されるかどうかと保存期間は、利用するサービスと設定によって異なります。たとえば、Pages Functionsのリアルタイムのログは保存されません(Pages Functionsのログ)。配信状態は、Resendの配信イベント(送信受付・配信・遅延・バウンスなど)で確認できます(Resend:Event Types)。

Vercel と比較(開発者目線)

同種の構成はVercelでも実装できます。選択は、利用するランタイムと依存ライブラリ、既存の環境、費用の条件で判断します。

表は横にスクロールできます →

▼ Vercel FunctionsとCloudflare Workersで確認すること
項目Vercel Functions(Node.jsランタイム)Cloudflare Workers
実行環境Node.jsランタイム。Node.jsのAPIを利用できるWorkersのランタイム。Node.jsのAPIは一部に対応
依存ライブラリNode.js向けのライブラリを使いやすい使うライブラリがWorkersで動くかを事前に確認する
既存の環境Vercelでサイトを運用している場合にまとめやすいCloudflareでサイトやDNSを運用している場合にまとめやすい
費用の条件プランと、実行時間やメモリなどの利用量で決まるプランと、リクエスト数やCPU時間などの利用量で決まる

どちらが速いか、安いかは、処理の内容、利用量、契約プランによって変わります。メール送信だからといって一方が常に有利とは限りません。無料枠と課金条件は変更されるため、金額は各社の料金ページで最新の条件を確認してください(Vercel:Node.jsランタイム)。

発展:WAF / CDN / Zero Trust への布石

フォームを公開した後も、Cloudflareの機能で保護を追加できます。ただし、使える機能と設定できる条件は、対象のURL・ドメイン・公開経路と契約プランによって異なります。アプリ側の入力検証やトークンの検証、運用の担当を置き換えるものではありません。

  • WAF:Cloudflareを経由するドメインへのリクエストに、条件に応じたルールを適用します。
  • レート制限:一定の時間内のリクエスト数に上限を設けます。ルールに使える条件や期間は、プランによって異なります(Rate limiting rules)。たとえば、同じ送信元から短い時間に一定の回数を超えたら一時的に遮断する、といったルールを、通常の利用者を遮断しない値で決めます。
  • Zero Trust:社内向けの管理画面を追加で作る場合に、利用者を認証してからアクセスを許可する仕組みを組み合わせられます。

どの対策も、導入しただけで安全になるわけではありません。対象の経路と設定値を決め、誤って遮断した場合の連絡先と確認の担当を決めておきます。

まとめ

静的サイトでも、フォームの受信処理をサーバー側に置けば問い合わせフォームを実装できます。受信処理はPages Functionsや別のWorkerで実装でき、画面・項目やデータの保存先がサービスの範囲で足りる場合は、外部のフォームサービスも選択肢です。どの方式でも、配信サービスの料金、コードと設定の保守、失敗したときの確認の担当は残るため、運用費と担当を含めて選びます。

自社の状況を確認する

  • □ 受付の方式(外部サービス・Pages Functions・別のWorker)と、公開する経路を決めた
  • □ 受付条件(メソッド・形式・サイズ)と入力値をサーバー側で検証している
  • □ Turnstileのトークンをサーバー側で検証し、失敗・失効・再使用を通過させない
  • □ 送信元は検証済みのドメイン、通知先はサーバー側で決め、秘密値をコードに書いていない
  • □ メールAPIのエラーやタイムアウトを成功と表示せず、再試行と重複防止の方法を決めた
  • □ メールAPIの受付とは別に、配信状態を確認する方法を決めた
  • □ 記録する項目と保存期間、確認する担当を決めた
  • □ 公開前に、正常な送信と異常時の動作を確認した

よくある質問 (FAQ)

Q. TurnstileはreCAPTCHAと何が違うのですか?

Turnstileは、訪問者に画像選択などのパズルを表示せずに判定できる仕組みとして提供されています。公式の説明では、フォームの入力内容などを取得・保存しないとされています。どちらを使う場合も、トークンはサーバー側で検証します。個人データの扱いは、各サービスのプライバシー条件と自社の方針で確認してください。

Q. 無料プランでどこまで耐えられますか?

無料枠の上限はサービスごとに異なり、変更される場合があります。通常時とキャンペーン時の送信数を見積もり、Workersとメール配信サービスの両方について、最新の料金、レート制限、上限を超えたときの動作を確認してください。

Q. ResendのFromアドレスはGmailでもいいですか?

Resendで送信するには、自分が管理するドメインを追加して検証する必要があり、Gmailなどの共有のドメインは送信元に使えません。送信元は、noreplyなど、検証済みの自社ドメインのアドレスにします。利用者のメールアドレスはFromに入れず、返信先として使う場合は検証した入力をReply-Toに設定します。

Q. Pages FunctionsとWorkers、結局どっちが良い?

既存の構成と運用の分担で選びます。既存のPagesサイトに受信処理を加え、同じプロジェクトで管理するならPages Functionsで実装できます。新しく作る場合や、サイトと受信処理を別々に公開・運用したい場合は、Workersを検討します。秘密値の保存やログの確認は、どちらでも使えます。

Q. 将来的にWAFを入れるタイミングは?

スパムが増えてから考えるのではなく、公開前に必要な対策を決めておきます。サーバー側の検証、入力の制限、多重送信の防止は、公開した時点から必要です。WAFやレート制限のルールは、対象のURLと公開経路、契約プランで使える条件を確認し、通常の利用者を遮断しない設定から始めます。

参考リンク / 出典

問い合わせフォームを、送信と運用の条件から整えます。

現在のサイトと環境、必要な入力項目、通知先、迷惑メールの対策、失敗したときに確認する担当を合意して、フォームを設計・改修する支援です。今の環境と権限を確認して、対応できるかと範囲を決めます。公開後の継続的な監視や運用の代行は、別に合意します。

問い合わせフォームの設計・改修の支援内容を見る

この分野の全体像

モダンWeb開発の構成判断:Next.jsとヘッドレスCMSをどう組み合わせるか

この分野の全体像と判断の順序をまとめた記事です。

全体像を読む

IT課題を、
優先順位から整理します。

Malakeは外部IT責任者として、現状の確認から実装・運用まで支援します。