DEV Community

Karin
Karin

Posted on • Originally published at khendrikse.netlify.app on

Send data to a node.js server with FormData, a comprehensive guide.

There are many different ways to send data from an HTML form to a server. I wanted to provide a deep dive into using the FormData JavaScript interface for anyone who might have seen it before but hasn't had the chance to dive into it. In this article, you'll learn how the content type multipart/form-data differs in use from alternative content types, how the FormData interface works, how you can use it to send data to the server, and how you can write a simple server to receive and use the data.

Different content types for sending data

Let's look at the following piece of HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Simple Form</title>
  </head>
  <body>
    <form id="myForm">
      <label for="name">Name:</label>
      <input type="text" id="name" name="name" />

      <label for="favorite-food">Favorite food:</label>
      <input type="text" id="favorite-food" name="favorite-food" />

      <input type="submit" value="Submit" />
    </form>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

It has a form element with two inputs. Each is a text input. What are the different ways we could send the data from this form to a server?

text/plain

If you don't specify a Content-Type header, text/plain is the default. Here is an example of sending the data from the form above to a server using text/plain:

<script>
  document
    .getElementById('myForm')
    .addEventListener('submit', function (event) {
      event.preventDefault();

      var elements = this.elements;

      const data = [...elements]
        .reduce((acc, curr) => {
          if (curr.name) {
            acc.push(curr.name + '=' + curr.value);
          }
          return acc;
        }, [])
        .join('&');

      fetch('/api/endpoint', {
        method: 'POST',
        headers: {
          'Content-Type': 'text/plain',
        },
        body: data,
      });
    });
</script>
Enter fullscreen mode Exit fullscreen mode

The data is sent as a string of query parameters. This is what the data would look like when it gets to the server:

'name=John&favorite-food=Pizza'
Enter fullscreen mode Exit fullscreen mode

For most uses, you may have to add some code on the server to parse the query parameters. While there are more convenient ways of sending data, it sure is simple! (and sometimes, all you truly need…). The problem is that once you want to start sending different types of data, it becomes really unflexible. You can only send text data, and you cannot send files.

application/x-www-form-urlencoded

There is also the form element's default behavior, which is using the application/x-www-form-urlencoded content type. This happens when we add a method="post" attribute to the form element. When someone submits the form, the data is sent to the server as a string of query parameters. But unlike text/plain, it is actually sent in the body of the request.

A side effect is that the page will reload or navigate to whatever path is given in the action="/" attribute. This is standard behavior for HTML when submitting a form. The navigational side-effect can be prevented by using JavaScript by calling event.preventDefault() in the submit event listener.

This way of sending data has decreased in popularity with the rise of JavaScript frameworks. In modern web apps, developers tend to use JavaScript to send the data to a server, wanting to avoid navigating or reloading the page. Still, though, it's a very common way of sending data that is still used often.

To send the data from the form above to a server using application/x-www-form-urlencoded, we can change the code from the original form by adding the following additional attributes:

<form id="myForm" method="post" action="/"></form>
Enter fullscreen mode Exit fullscreen mode

This would send the data from the inputs in the form with a POST method to the server residing at the root path /. This way of sending data also, by default, does not support uploading files.

application/json

With the rise of JavaScript frameworks and the need to send data to a server from within Single Page Applications (SPA's), the application/json content type is often used. This is how you could send data using the application/json content-type:

<script>
  document
    .getElementById('myForm')
    .addEventListener('submit', function (event) {
      event.preventDefault();

      var formData = {
        name: document.getElementById('name').value,
        favoriteFood: document.getElementById('favorite-food').value,
      };

      fetch('/api/endpoint', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      })
        .then((response) => response.json())
        .then((data) => console.log('Success:', data))
        .catch((error) => console.error('Error:', error));
    });
</script>
Enter fullscreen mode Exit fullscreen mode

With this kind of content type it is still not natively supported to upload files. It would mean you'd have to encode the file as a base64 string and send it as a string. This is not very efficient, and it is not the way to go if you want to upload large files.

multipart/form-data

HTML forms used to be used primarily for text data. But as the web became more interactive, it became necessary for HTML forms to support file uploads, too. In 1995, a Request For Comment (RFC) called RFC-1867 was published that proposed extending the functionality of HTML forms to start supporting file uploads. This is how we arrive at the multipart/form-data content type.

This content type has made it possible to deal with multiple types of data in one request (thus, the name multipart). It can handle text data but also binary data, which is necessary for file uploads. This makes this content type a native way to upload files. While previous content types could be tweaked to send files by converting files into a Base64 string, this is not necessary with multipart/form-data.

This is what it would look like to send the data from the form above to a server using multipart/form-data:

<script>
  document
    .getElementById('myForm')
    .addEventListener('submit', function (event) {
      event.preventDefault();

      var formData = new FormData(this);

      fetch('your-server-endpoint', {
        method: 'POST',
        body: formData,
      })
        .then((response) => response.json())
        .then((data) => console.log('Success:', data))
        .catch((error) => console.error('Error:', error));
    });
</script>
Enter fullscreen mode Exit fullscreen mode

Diving into FormData

Now that we've arrived at the topic of FormData let's take a deep dive into it. There are two parts to FormData: the multipart/form-data content type and the FormData object. The multipart/form-data content type is a piece of metadata that is added to the request header so the server knows how to handle the data. Looking at the previous example, you can see that we're not actively adding this header to our fetch(). This is because if fetch detects that we're sending the FormData interface, it automatically adds that header for us.

The FormData interface is a JavaScript interface that makes it really easy to create a key/value pair that can be sent to a server.

Creating a FormData object

Because FormData is a JavaScript interface, we can create a new instance of it using new. There are three ways that we can instantiate a new FormData object:

// Without any arguments, creates an empty FormData object
const formData = new FormData();

// Pass in a form element to create a FormData object
const formData = new FormData(document.getElementById('myForm'));

// Passing in a form element together with the submit button from the form.
// If the button has a name attribute, it will be included in the FormData object.
const formData = new FormData(
  document.getElementById('myForm'),
  document.getElementById('submit-button')
);
Enter fullscreen mode Exit fullscreen mode

Available methods on FormData

At the time of writing, FormData supports nine methods on its instance: append(), delete(), entries(), get(), getAll(), has(), keys(), set() and values().

// An empty form data object
const formData = new FormData();

// Using append() we can add a new key/value pair to the FormData object
formData.append('name', 'Jo');
formData.append('favorite-food', 'Pizza');

// Using delete() we can delete a key/value pair from the FormData object
formData.delete('name');
formData.append('name', 'Joan');

// Using entries() we can get an iterator that contains all the key/value pairs
for (const pair of formData.entries()) {
  console.log(`${pair[0]}, ${pair[1]}`);
}

// output:
// name, Joan
// favorite-food, Pizza

// Using get() we can get the value of a key
formData.get('name'); // returns 'Joan'

// Using getAll() we can get all the values of a key
formData.getAll('name'); // returns ['Joan']

// Using has() we can check if a key exists
formData.has('name'); // returns true

// Using keys() we can get an iterator that contains all the keys
for (const key of formData.keys()) {
  console.log(key);
}

// output:
// name
// favorite-food

// Using set() we can set the value of an existing key or create a new key/value pair if the key does not exist yet.
formData.set('name', 'Jo');

// Using values() we can get an iterator that contains all the values
for (const value of formData.values()) {
  console.log(value);
}

// output:
// Jo
// Pizza
Enter fullscreen mode Exit fullscreen mode

Sending a FormData object to a server

In our previous example, this is how we sent the data from our HTML form to the server:

document.getElementById('myForm').addEventListener('submit', function (event) {
  event.preventDefault();

  var formData = new FormData(this);

  fetch('/api/endpoint', {
    method: 'POST',
    body: formData,
  })
    .then((response) => response.json())
    .then((data) => console.log('Success:', data))
    .catch((error) => console.error('Error:', error));
});
Enter fullscreen mode Exit fullscreen mode

Walking through this:

  1. We have added a submit event listener to the form element.
  2. On the submit event, we prevent the default behavior of the form.
  3. We create a new FormData object using this as an argument. Because we added an event listener to the form element in JavaScript, the function we provided as the event handler gets executed in the context of the element that the event was attached to. This means that this inside the function refers to that element.
  4. We then send the FormData object to the server using fetch(). Because we're sending a FormData object, fetch() automatically adds the multipart/form-data content type to the request header.

Receiving FormData on the server

Without a file upload

Now that we've seen how we can send FormData to a server let's look at the simplest way possible to receive this on a node.js server. Node.js does not support parsing the multipart/form-data content type by default, which means that we need to add a little piece of middleware called multer.

Multer is middleware that you can use for handling multipart/form-data requests. Middleware is code that you can add to your server that runs between the request and the server's response, adding some automation to the process so you don't have to manually handle parsing the request. There are other pieces of middleware that could help with handling multipart/form-data requests, but for this example, we'll go with Multer.

Let's look at the simplest example with node.js, express and multer:

const express = require('express');
const multer = require('multer');
const app = express();

const upload = multer();

app.post('/api/endpoint', upload.none(), (req, res) => {
  const { body } = req;
  console.log({ body });
  res.send('Data received successfully');
});

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

The steps we took to write this code:

  1. We imported express and multer.
  2. We created an instance of express and assigned it to the variable app.
  3. We created an instance of multer and assigned it to the variable upload.
  4. We created a route for a POST request to the /api/endpoint path.
  5. We used multer's none() method to tell multer that we're not expecting any files to be uploaded.
  6. We added a callback function that will be executed when a request is made to the /api/endpoint path.
  7. Inside the callback function we log the data that we received from the request.

With a file upload

Now, let's set up the logic to also upload a file using this form. We'll first need to add a file input to our form:

<form id="myForm" method="post" action="/" enctype="multipart/form-data">
  <label for="name">Name:</label>
  <input type="text" id="name" name="name" />

  <label for="favorite-food">Favorite food:</label>
  <input type="text" id="favorite-food" name="favorite-food" />

  <label for="file">File:</label>
  <input type="file" id="file" name="file" />

  <input type="submit" value="Submit" />
</form>
Enter fullscreen mode Exit fullscreen mode

If we tried to send this to the current implementation of our server endpoint, it would give us the error MulterError: Unexpected field. This is because we are still using upload.none() in our middleware. We need to change this to upload.single('file') to tell multer that we're expecting a single file to be uploaded. Multer will add a file property to the req object that we can use to access the file that was uploaded. Let's log that file as well.

const express = require('express');
const multer = require('multer');
const app = express();

const upload = multer();

app.post('/api/endpoint', upload.single('file'), (req, res) => {
  const { body, file } = req;
  console.log({ body, file });
  res.send('Data received successfully');
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

With this setup, it should be possible to log both your body and the data of the file you've sent. Hopefully, this clarifies the usage and background of FormData. Happy coding!

Top comments (3)

Collapse
 
fyodorio profile image
Fyodor

Cool to see that, this topic is quite underpaid with attention in our SPA world πŸ™‚

Collapse
 
khenhey profile image
Karin

Thanks!

Collapse
 
sc1entifik profile image
Sc1entifik

The thing you didn't mention here that I can't seem to find ANYWHERE going elbow deep into documentations and tutorials is this:

How do I

e.preventDefault()
Enter fullscreen mode Exit fullscreen mode

for a form submission, then add data to the form via form.append so I can add extra data that is not user submitted to the form, and then submit that form so I can access that data on my node server AND then redirect the user to another page?

For example if I don't

e.preventDefault()
Enter fullscreen mode Exit fullscreen mode

and go the urlencoded() route my page will get redirected from the server side using

res.sendFile(some.html)
Enter fullscreen mode Exit fullscreen mode

without me being able to add extra key:value pairs to the form such as

form.append("postKey","passphrase");
Enter fullscreen mode Exit fullscreen mode

but once I

e.preventDefault()
Enter fullscreen mode Exit fullscreen mode

I can create, and send a FormData object, adding the extra fields I want to it such as

form.append("postKey","passphrase");
Enter fullscreen mode Exit fullscreen mode

, using a fetch request. Once this Content-Type header is no longer the form default however how do I then use something like res.sendFile() server side to then redirect the user? When I try to do this using either mulitpart form OR json I just run into a brick wall. I can access all the data server side and make conditionals using the request body however when I try to use

res.sendFile(some.html);
Enter fullscreen mode Exit fullscreen mode

to redirect the user I get no errors and nothing happens.