JavaScript – Post form data to a web API asynchronously

You can post form data asynchronously by using fetch() + FormData, like this:

async function saveMovie(movieForm) { const response = await fetch("https://localhost:12345/movies/", { method: 'POST', body: new FormData(movieForm) }); if (!response.ok) throw `Error response code: ${response.status}` //Optionally return the response content return await response.json(); }
Code language: JavaScript (javascript)

FormData parses the form element into an array of key/value pairs for you.

This results in sending the following request:

POST https://localhost:12345/movies/ Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryhdnKiqD1Rbz4E9Zr Body (raw): ------WebKitFormBoundaryhdnKiqD1Rbz4E9Zr Content-Disposition: form-data; name="title" Dune ------WebKitFormBoundaryhdnKiqD1Rbz4E9Zr Content-Disposition: form-data; name="yearReleased" 2021 ------WebKitFormBoundaryhdnKiqD1Rbz4E9Zr Content-Disposition: form-data; name="score" 9 ------WebKitFormBoundaryhdnKiqD1Rbz4E9Zr--
Code language: plaintext (plaintext)

When you set the request body to a FormData object, it sets the content-type to multipart/form-data. You can post the data in other formats depending on what the web API expects. In this article, I’ll show how to post in these other formats. At the end, I’ll show the form + event handler.

Posting form data as application/x-www-form-urlencoded

To post the form data as application/x-www-form-urlencoded, you can use a combo of FormData + URLSearchParams:

async function saveMovie(movieForm) { const response = await fetch("https://localhost:12345/movies/", { method: 'POST', body: new URLSearchParams(new FormData(movieForm)) }); if (!response.ok) throw `Error response code: ${response.status}` //Optionally return the response content return await response.json(); }
Code language: JavaScript (javascript)

FormData parses the form element for you, and then URLSearchParams formats the data like a query string (URL-encoded key/value pairs separated by a ‘&’).

This sends the following request:

POST https://localhost:12345/movies/ Content-Type: application/x-www-form-urlencoded Body (raw): title=Dune&yearReleased=2021&score=9
Code language: plaintext (plaintext)

When you set the request body to a URLSearchParams object, it sets the content-type to application/x-www-form-urlencoded.

Posting form data as JSON

If the web API expects JSON, then you’ll need to convert the form fields to JSON and send the request with fetch(), like this:

async function saveMovie(movieJson) { const response = await fetch("https://localhost:12345/movies/", { method: 'POST', body: movieJson, headers: { 'Content-Type':'application/json'} }); if (!response.ok) throw `Error response code: ${response.status}` //Optionally return the response content return await response.json(); }
Code language: JavaScript (javascript)

Notice that you need to explicitly add the Content-Type request header and set it to application/json. This sends the following request:

POST https://localhost:12345/movies/ Content-Type: application/json Body: { "title": "Dune", "yearReleased": 2021, "score": 9 }
Code language: plaintext (plaintext)

Form fields to JSON

Sending the JSON data is the straightforward part. Converting the form fields to JSON is the hard part. It can be tricky depending on the complexity of your form and what the web API expects. I’ll show a few examples below.

Here’s one way to convert form fields to JSON:

const json = JSON.stringify(Object.fromEntries(new FormData(movieForm)));
Code language: JavaScript (javascript)

This generates the following JSON:

{ "title": "Dune", "yearReleased": "2021", "score": "9" }
Code language: JSON / JSON with Comments (json)

Notice that all of the values are strings, including the number fields (yearReleased/score). This is because when FormData parses the input fields from the form element, it gets the values as strings. Depending on how the web API is configured, this might cause it to return an error response due to expecting different types.

Another approach is to manually map the form fields to an object, then convert that to JSON (instead of relying on the automatic mapping):

const json = JSON.stringify( { title: movieForm.title.value, yearReleased: Number(movieForm.yearReleased.value), score: Number(movieForm.score.value) });
Code language: JavaScript (javascript)

This generates the following JSON:

{ "title": "Dune", "yearReleased": 2021, "score": 9 }
Code language: JSON / JSON with Comments (json)

Notice that the yearReleased/score fields have numeric values instead of strings.

Form element + event handler

All of the code snippets have shown the saveMovie() function, which is what sends the request. This section puts that into context by showing the form element and the event handling code.

First, here is the form element:

<form class="movieForm"> <h2>Add a movie</h2> <label for="title">Title</label> <input type="text" id="title" name="title"> <label for="yearReleased">Year Released</label> <input type="number" max="10000" min="1900" id="yearReleased" name="yearReleased"> <label for="score">Score</label> <input type="number" min="0.00" max="10.00" step="0.5" id="score" name="score"> <hr> <button type="submit">Save</button> </form>
Code language: HTML, XML (xml)

Note: FormData will only map input fields that have the name attribute.

When the user clicks the submit button on the form, it fires the submit event. Here is the submit event handler:

const movieForm = document.querySelector('.movieForm'); movieForm.addEventListener('submit', event => { event.preventDefault(); saveMovie(movieForm) .then(responseContent => console.log(responseContent)) .catch(err => console.error(err)) });
Code language: JavaScript (javascript)

The saveMovie() function sends the request and returns the response content (or throws an exception if there’s a problem). It uses async/await, because this keeps thing clean at its level. The event handler function has to use the output from saveMovie() to determine the next step (in this example, it’s just logging the outcomes). It uses the Promise continuation style (chained then / catch) because that keeps things clean based on what it has to do.

Leave a Comment