Cloudflare Docs
Pages
Visit Pages on GitHub
Set theme to dark (⇧+D)

HTML forms

In this tutorial, you will create a simple <form> using plain HTML and CSS and deploy it to Cloudflare Pages. While doing so, you will learn about some of the HTML form attributes and how to collect submitted data within a Worker.

This tutorial will make heavy use of Cloudflare Pages and its Workers integration . Refer to the Get started guide guide to familiarize yourself with the platform.

Overview

On the web, forms are a common point of interaction between the user and the web document. They allow a user to enter data and, generally, submit their data to a server. A form is comprised of at least one form input, which can vary from text fields to dropdowns to checkboxes and more.

Each input should be named – using the name attribute – so that the input’s value has an identifiable name when received by the server. Additionally, with the advancement of HTML5, form elements may declare additional attributes to opt into automatic form validation. The available validations vary by input type; for example, a text input that accepts emails (via type=email) can ensure that the value looks like a valid email address, a number input (via type=number) will only accept integers or decimal values (if allowed), and generic text inputs can define a custom pattern to allow. However, all inputs can declare whether or not a value is required.

Below is an example HTML5 form with a few inputs and their validation rules defined:

<form method="POST" action="/submit">
  <input type="text" name="fullname" pattern="[A-Za-z]+" required />
  <input type="email" name="email" required />
  <input type="number" name="age" min="18" required />

  <button type="submit">Submit</button>
</form>

If an HTML5 form has validation rules defined, browsers will automatically check all rules when the user attempts to submit the form. Should there be any errors, the submission is prevented and the browser displays the error message(s) to the user for correction. The <form> will only POST data to the /submit endpoint when there are no outstanding validation errors. This entire process is native to HTML5 and only requires the appropriate form and input attributes to exist — no JavaScript is required.

Form elements may also have a <label> element associated with them, allowing you to clearly describe each input. This is great for visual clarity, of course, but it also allows for more accessible user experiences since the HTML markup is more well-defined. Assistive technologies directly benefit from this; for example, screen readers can announce which <input> is focused. And when a <label> is clicked, its assigned form input is focused instead, increasing the activation area for the input.

To enable this, you must create a <label> element for each input and assign each <input> element and unique id attribute value. The <label> must also possess a for attribute that reflects its input’s unique id value. Amending the previous snippet should produce the following:

<form method="POST" action="/submit">
  <label for="i-fullname">Full Name</label>
  <input id="i-fullname" type="text" name="fullname" pattern="[A-Za-z]+" required />

  <label for="i-email">Email Address</label>
  <input id="i-email" type="email" name="email" required />

  <label for="i-age">Your Age</label>
  <input id="i-age" type="number" name="age" min="18" required />

  <button type="submit">Submit</button>
</form>

When this <form> is submitted with valid data, its data contents are sent to the server. You may customize how and where this data is sent by declaring attributes on the form itself. If you do not provide these details, the <form> will POST the data to the current URL address, which is rarely the desired behavior. To fix this, at minimum, you need to define an action attribute with the target URL address, but declaring a method is often recommended too, even if you are redeclaring the default POST value.

By default, HTML forms send their contents in the application/x-www-form-urlencoded MIME type. This value will be reflected in the Content-Type HTTP header, which the receiving server must read to determine how to parse the data contents. You may customize the MIME type through the enctype attribute. For example, to accept files (via type=file), you must change the enctype to the multipart/form-data value:

<form method="POST" action="/submit" enctype="multipart/form-data">
  <label for="i-fullname">Full Name</label>
  <input id="i-fullname" type="text" name="fullname" pattern="[A-Za-z]+" required />

  <label for="i-email">Email Address</label>
  <input id="i-email" type="email" name="email" required />

  <label for="i-age">Your Age</label>
  <input id="i-age" type="number" name="age" min="18" required />

  <label for="i-avatar">Profile Picture</label>
  <input id="i-avatar" type="file" name="avatar" required />

  <button type="submit">Submit</button>
</form>

Because the enctype changed, the browser changes how it sends data to the server too. The Content-Type HTTP header will reflect the new approach and the HTTP request’s body will conform to the new MIME type. The receiving server must accommodate the new format and adjust its request parsing method.

Live example

The rest of this tutorial will focus on building an HTML form on Pages, including a Worker to receive and parse the form submissions.

Setup

To begin, create a new GitHub repository. Then create a new local directory on your machine, initialize git, and attach the GitHub location as a remote destination:

# create new directory
$ mkdir new-project
# enter new directory
$ cd new-project
# initialize git
$ git init
# attach remote
$ git remote add origin git@github.com:<username>/<repo>.git
# change default branch name
$ git branch -M main

You may now begin working in the new-project directory you created.

Markup

The form for this example is fairly straightforward. It includes an array of different input types, including checkboxes for selecting multiple values. The form also does not include any validations so that you may see how empty and/or missing values are interpreted on the server.

You will only be using plain HTML for this example project. You may use your preferred JavaScript framework, but raw languages have been chosen for simplicity and familiarity – all frameworks are abstracting and/or producing a similar result.

Create a public/index.html in your project directory. All front-end assets will exist within this public directory and this index.html file will serve as the home page for the website.

Copy and paste the following content into your public/index.html file:

<html lang="en">
  <head>
    <meta charset="utf8" />
    <title>Form Demo</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
  </head>
  <body>
    <form method="POST" action="/api/submit">
      <div class="input">
        <label for="name">Full Name</label>
        <input id="name" name="name" type="text" />
      </div>

      <div class="input">
        <label for="email">Email Address</label>
        <input id="email" name="email" type="email" />
      </div>

      <div class="input">
        <label for="referers">How did you hear about us?</label>
        <select id="referers" name="referers">
          <option hidden disabled selected value></option>
          <option value="Facebook">Facebook</option>
          <option value="Twitter">Twitter</option>
          <option value="Google">Google</option>
          <option value="Bing">Bing</option>
          <option value="Friends">Friends</option>
        </select>
      </div>

      <div class="checklist">
        <label>What are your favorite movies?</label>
        <ul>
          <li>
            <input id="m1" type="checkbox" name="movies" value="Space Jam" />
            <label for="m1">Space Jam</label>
          </li>
          <li>
            <input id="m2" type="checkbox" name="movies" value="Little Rascals" />
            <label for="m2">Little Rascals</label>
          </li>
          <li>
            <input id="m3" type="checkbox" name="movies" value="Frozen" />
            <label for="m3">Frozen</label>
          </li>
          <li>
            <input id="m4" type="checkbox" name="movies" value="Home Alone" />
            <label for="m4">Home Alone</label>
          </li>
        </ul>
      </div>

      <button type="submit">Submit</button>
    </form>
  </body>
</html>

This HTML document will contain a form with a few fields for the user to fill out. Because there is no validation rules within the form, all fields are optional and the user is able to submit an empty form. For this example, this is intended behavior.

Worker

The HTML form is complete and ready for deployment. When the user submits this form, all data will be sent in a POST request to the /api/submit URL. This is due to the form’s method and action attributes. However, there is currently no request handler at the /api/submit address. You will now create it.

Cloudflare Pages offers a Functions feature, which allows you to define and deploy Workers for dynamic behaviors.

Functions are linked to the functions directory and conveniently construct URL request handlers in relation to the functions file structure. For example, the functions/about.js file will map to the /about URL and functions/hello/[name].js will handle the /hello/:name URL pattern, where :name is any matching URL segment. Refer to the Functions routing documentation for more information.

To define a handler for /api/submit, you must create a functions/api/submit.js file. This means that your functions and public directories should be siblings, with a total project structure similar to the following:

├── functions
│   └── api
│       └── submit.js
└── public
    └── index.html

The <form> will send POST requests, which means that the functions/api/submit.js file needs to export an onRequestPost handler:

---
filename: functions/api/submit.js
---
/**
 * POST /api/submit
 */
export async function onRequestPost(context) {
  // TODO: Handle the form submission
}

The context parameter is an object filled with several values of potential interest. For this example, you only need the Request object, which can be accessed through the context.request key.

As mentioned, a <form> defaults to the application/x-www-form-urlencoded MIME type when submitting. And, for more advanced scenarios, the enctype="multipart/form-data" attribute is needed. Luckily, both MIME types can be parsed and treated as FormData. This means that with Workers – which includes Pages Functions – you are able to use the native Request.formData parser.

For illustrative purposes, the example application’s form handler will reply with all values it received. A Response must always be returned by the handler, too:

---
filename: functions/api/submit.js
---
/**
 * POST /api/submit
 */
export async function onRequestPost(context) {
  try {
    let input = await context.request.formData();
    let pretty = JSON.stringify([...input], null, 2);
    return new Response(pretty, {
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
    });
  } catch (err) {
    return new Response('Error parsing JSON content', { status: 400 });
  }
}

With this handler in place, the example is now fully functional. When a submission is received, the Worker will reply with a JSON list of the FormData key-value pairs.

However, if you want to reply with a JSON object instead of the key-value pairs (an Array of Arrays), then you must do so manually. Recently, JavaScript added the Object.fromEntries utility. This works well in some cases; however, the example <form> includes a movies checklist that allows for multiple values. If using Object.fromEntries, the generated object would only keep one of the movies values, discarding the rest. To avoid this, you must write your own FormData to Object utility instead:

---
filename: functions/api/submit.js
---
/**
 * POST /api/submit
 */
export async function onRequestPost(context) {
  try {
    let input = await context.request.formData();

    // Convert FormData to JSON
    // NOTE: Allows multiple values per key
    let output = {};
    for (let [key, value] of input) {
      let tmp = output[key];
      if (tmp === undefined) {
        output[key] = value;
      } else {
        output[key] = [].concat(tmp, value);
      }
    }

    let pretty = JSON.stringify(output, null, 2);
    return new Response(pretty, {
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
    });
  } catch (err) {
    return new Response('Error parsing JSON content', { status: 400 });
  }
}

The final snippet (above) allows the Worker to retain all values, returning a JSON response with an accurate representation of the <form> submission.

Deployment

You are now ready to deploy your project.

If you have not already done so, save your progress within git and then push the commit(s) to the GitHub repository:

# Add all files
$ git add -A
# Commit w/ message
$ git commit -m "working example"
# Push commit(s) to remote
$ git push -u origin main

Your work now resides within the GitHub repository, which means that Pages is able to access it too.

If this is your first Cloudflare Pages project, refer to the Get started guide for a complete walkthrough. After selecting the appropriate GitHub repository, you must configure your project with the following build settings:

  • Project name – Your choice
  • Production branch – main
  • Framework preset – None
  • Build command – None / Empty
  • Build output directorypublic

After clicking the Save and Deploy button, your Pages project will begin its first deployment. When successful, you will be presented with a unique *.pages.dev subdomain and a link to your live demo.

In this tutorial, you built and deployed a website and its back-end logic using Cloudflare Pages with its Workers integration. You created a static HTML document with a form that communicates with a Worker handler to parse the submission request(s).

If you would like to review the full source code for this application, you can find it on GitHub.