Eben Gilkenson

Basic Contact Form with Cloudflare Workers

A contact form just needs to take form data and get it to whoever needs it. For the small sites that I work on, this has been done so easily for years with the PHP mail() function. Moving to a serverless platform becomes somewhat more involved, but provides opportunities to add features that would be harder to implement with a PHP script.

The Options

For a serverless form handler, Cloudflare Workers aren’t really the first place you would probably look. Not being a full Node environment, they cannot run an SMTP client like nodemailer, so you’re dependent upon a third party to actually deliver any email. Here are some other options that could be considered:

I’ve chosen to go the Cloudflare route because I’m already hosting this site on Cloudflare Pages and it seems like as good a place as any to get started with Workers. Also, Cloudflare’s bot detection should eliminate the need for using any type of captcha.

Michael Wanyoike has already covered much of this in a great, in-depth tutorial on SitePoint. I’m taking some ideas from his example and developing my own version that will ultimately use Cloudflare’s KV storage to collect submissions and then send them in a single daily digest email. But first, I just need to get the basic form handling up and running.

Workers with Cloudflare Pages

When using Cloudflare Pages as I am, setting up a worker is a little different from having one that runs on its own. Rather than having a separate project that needs to be published using the Wrangler CLI, a Pages Function can just be placed in a functions directory in the project root. I’m putting mine in an api subdirectory, but that’s not a requirement.

There are a few important differences when using a Worker inside Cloudflare Pages:

The Worker

This single file is all that is needed to accept a JSON payload and use it to send an email. I’m using Mailgun in this case, but this should be easily adaptable to most services that allow you to send email via a REST API.

/* /functions/api/form-submit/index.js */

export async function onRequestPost({ request, env }) {
  const json = await request.json();

  const body = `
    Name: ${json.firstName} ${json.lastName}
    Email: ${json.email}

  const data = {
    to: env.TO_EMAIL_ADDRESS,
    from: env.FROM_EMAIL_ADDRESS,
    subject: `New Contact from ${json.email}`,
    text: body,

  const submitUrlString = encodeURI(
      .map((param) => param.join("="))

  const init = {
    method: "POST",
    headers: {
      Authorization: "Basic " + btoa("api:" + env.MAILGUN_API_KEY),
      "Content-Type": "application/x-www-form-urlencoded",
      "Content-Length": submitUrlString.length.toString(),
    body: submitUrlString,

  const result = await fetch(`${env.MAILGUN_API_BASE_URL}/messages`, init);

  if (result.status === 200) {
    return new Response(JSON.stringify({ message: "OK" }), {
      headers: { "content-type": "application/json" },
      status: 200,
  } else {
    return new Response(
      JSON.stringify({ message: "Message submission failed!", result }),
        headers: { "content-type": "application/json" },
        status: 400,

That Authorization header might look a little odd. Basic Authentication is really intended for use with a username and password. but Mailgun’s authentication scheme uses “api” as the username and the API key as the password. The btoa() function is built into Javascript and encodes the string as base64, which is required by the Basic Authentication standard.

What’s Next?

Eventually, I need add server-side validation, provide better responses for various failure points and allow for accepting regular HTML form data, so I can accept submissions without the need for client-side Javascript.

However, I’m publishing this in its current form as an example of the bare minimum required to process form submissions using Cloudflare Pages Functions.

← Cloudflare Workers