Collecting form responses with Nuxt + Cloudflare Pages

Date Posted: 11-08-2026


Recently I wanted to learn how to collect form responses on my sites which use the Nuxt (v3) framework and are hosted on Cloudflare Pages. There's a number of paid services available for this but I didn't want to pay a recurring subscription for the lifetime of my forms, and wasn't happy with the free tier limitations of any of these options (specifically limited 30 day retention of responses).

However, I've already been using a basic Cloudflare database as part of Nuxt Content v3 to query the blog posts on my website, surely it would be possible store responses in one of these? After a lot of research, trial and error, and more AI usage than I'd like to admit 😭😭. I've finally figured out how it works! That said, I also found there wasn't much documentation and resources on the internet for this topic, so I hope to fill that gap by writing about how I implemented it in this post!

> About AI usage for this post


RequirementsLimitations
- Nuxt site (tested on v4)
- Deploying to Cloudflare pages
- There is a (generous) limit to Cloudflare D1 databases which you are extremely unlikely to hit but worth keeping in mind: https://developers.cloudflare.com/d1/platform/pricing/
- Form does not work during development, only on the live website

Step 1: Make your HTML form

For the most part this step is just creating a normal HTML form, make sure all your form elements are within the <form> tags, that the method is set to POST, and we'll come back to the action in step 3.

Your form will probably look a bit different, but this is what I'll be using for the examples. A form with an email field and a name field:

<form action="/api/form-test" method="POST">
  <label>Email: <input type="email" name="email" /></label>
  <label>Name: <input type="text" name="name" /></label>
  <button type="submit">Submit</button>
</form>

Step 2: Creating and binding a Cloudflare database

Creating the database

Login into your Cloudflare dashboard and in the left sidebar select Storage & databases -> D1 SQLite Database. If there is already a database listed here from another project you can use it or otherwise press the top right hand button to create a new database. The name can be anything you want~

Once you've got your database you can click into it and click Explore Data to have a look inside

Create the table for our form responses

To start with, your database will be empty. Before we can accept form responses we need to make a table to hold them in. The fastest way to do this is with an SQL query if you have experience with that. Alternatively you can use the "+ New" button to create one manually too. You'll want this to at least contain a column for each form field.

Below is the SQL query I used for the example, I have also added an id and created_at field too.

CREATE TABLE contact_submissions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    name TEXT,
    email TEXT
);

Binding the database

Now navigate to your pages (Left sidebar -> Compute -> Workers & Pages), select your site and go to the Settings tab. At this point, have a look at the Bindings header and click "+ Add". In the menu that pops up, select "D1 Database", in "variable name" call it DB (it is possible to use a different name however the default name DB will work without extra configuration) and pick the database you just created in the "D1 Database" field

Step 3: Connect your form to the database with a server route

In order to send the form response to the database we need to write code that runs on the server (since only the server has the ability to talk to the database). To do this we need to create a Server Route

If you don't already have it create new folders in the root of your project called server, routes and api each nested within the last. Finally create a script in api, you can call it anything but for this example, I'll be using form-test.js. Once you're done, the project structure should look something like this:

components/
-- (components go here)
pages/
-- (pages go here)
server/
-- routes/
   -- api/
      -- form-test.js

Remember that action on the form in Step 1? we want that action to point to this script - but we should only include the path after routes/ so in this example we want action="/api/form-test" in the HTML form

form-test.js

This file will be in charge of receiving the data from the form and putting in our database!

When the user submits the form, they'll be sent to this script along with their form responses. Those form responses and other information about the users request are all packaged into event so our job is to write code to handle that event.

For this task we'll start with the aptly named defineEventHandler() function. We have to have this since its what will get called when we load the page.

export default defineEventHandler(async (event) => {
    // code goes here
});

next we'll add a try statement that holds the readFormData() function to get the submitted data from the form. The try statement allows us to run a separate catch block of code if anything goes wrong when we are trying the former block of code.

export default defineEventHandler(async (event) => {
    try {
        const formData = await readFormData(event);
    }
    catch (error) {
        // runs if the try block fails
    }
});

now the users responses have been put into a FormData object (functions much like a dictionary) ready for us to query. I have also added code to check if the name and email fields have been filled. While you can also do this through HTML form validation, it is possible for users to spend malformed data by editing the request manually. That is also why there is some clean-up done on the name and email fields.

export default defineEventHandler(async (event) => {
    try {
        const formData = await readFormData(event);

        const email = formData.get("email")?.toString().trim();
        const name = formData.get("name")?.toString().trim();
    
        if (!email || !name) {
          return new Response("Please enter a name an email");
        }
    }
    catch {
        
    }
});

With all the information we need, we are ready to try and put it in the database, first we get a reference to it, then we write an SQL statement to add the line to the table.

const db = event.context.cloudflare.env.DB;
await db
  .prepare(`INSERT INTO contact_submissions (name, email) VALUES (?, ?)`)
  .bind(name, email)
  .run();

Specifically of interest here is the prepare() and bind() functions, the prepare is our base SQL query without the actual name and email data we want to add, the ? symbols are placeholders that we replace when we use the bind() function, they are replaced in the same order as the arguments are given unless you follow the ? with a number. (more details on this page)

Finally the rest of the code decides what we will output to the user, if anything went wrong in the try statement we return an error, and if everything worked we return a success! That brings our final complete code to:

export default defineEventHandler(async (event) => {
  try {
    const formData = await readFormData(event);

    const email = formData.get("email")?.toString().trim();
    const name = formData.get("name")?.toString().trim();

    if (!email || !name) {
      return new Response("Please enter a name an email");
    }

    const db = event.context.cloudflare.env.DB;
    await db
      .prepare(`INSERT INTO contact_submissions (name, email) VALUES (?, ?)`)
      .bind(name, email)
      .run();

    return new Response("Yay! data has been sent!");

  } catch (error) {
    console.error("Form submission error:", error);

    return new Response("mrowww something broke. (or you are running on a dev environment?)");
  }
});

Now all that's left to do is push your site to Cloudflare and everything should be done! You can view and query the responses through the Explore Data view used in Step 2. With this figured out, maybe you might see new ways to interact with my site soon~ meow miaw mrrrp nya~


RSS Feed

Thanks for reading! If you'd like to be notifed when I publish new posts, subscribe to my rss feed by putting this link into your RSS reader

Sorry but the RSS feed is broken at the moment 😢, still feel free to subscribe to it if you want notifications once its fixed

https://tabbycat.dev/blog/rss.xml