FormData
When you submit an HTML form the old-fashioned way, the browser packs up every field, encodes it, and ships it to the server for you. FormData gives you that same machinery from JavaScript, so you can build the payload yourself and send it with fetch — form fields, uploaded files, dynamically generated images, and any mix of those.
The object is defined by the FormData spec, and its whole purpose is to represent the key/value pairs of an HTML form.
The constructor takes one optional argument:
let formData = new FormData([form]);
Pass an HTML <form> element and FormData walks the form, reading every named control and copying its current value. Pass nothing and you get an empty object you fill in by hand.
The reason FormData matters: network methods like fetch accept it directly as a request body. The browser then serializes it and sets the request’s Content-Type to multipart/form-data automatically. To the server, an ordinary <form method="post"> submission and a fetch carrying FormData look identical.
Sending a simple form
Start with a plain form and no files. Grabbing the fields and posting them is close to a one-liner.
<form id="formElem">
<input type="text" name="name" value="Maya">
<input type="text" name="surname" value="Vance">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
let response = await fetch('/article/formdata/post/user', {
method: 'POST',
body: new FormData(formElem)
});
let result = await response.json();
alert(result.message);
};
</script>
Two details do the heavy lifting here:
e.preventDefault()stops the browser’s built-in form submission (which would reload the page or navigate away). You want to handle the request yourself.new FormData(formElem)reads the form’s current field values at that moment, so whatever the user typed is captured.
The server code is left out — it accepts the POST and answers with something like “User saved”. The point is the client side: one FormData, one fetch, done.
Instead of sending the data anywhere, the demo below just builds new FormData(form) and lists what it captured. Type in the fields and press Capture — notice the un-named input and the disabled one never show up, exactly as they wouldn’t in a native submit.
FormData methods
Once you have a FormData object, you can read and reshape its fields.
formData.append(name, value)— add a field with the givennameandvalue.formData.append(name, blob, fileName)— add a field as though it came from<input type="file">. The third argument is the file name (the name of the file as it would appear in the user’s filesystem), not the form field name.formData.delete(name)— remove the field with the givenname.formData.get(name)— return the value of the field with the givenname.formData.has(name)— returntrueif a field with thatnameexists, otherwisefalse.
A form is allowed to hold several fields sharing one name (think of a group of checkboxes). Because of that, calling append again with an existing name adds another field rather than replacing the old one.
When you want exactly one field per name, use set. It has the same signature as append, but it first removes every field with that name and then adds a fresh one:
formData.set(name, value)formData.set(name, blob, fileName)
append(‘x’,‘b’)
set(‘x’,‘b’)
You can also loop over the fields with for..of. Each iteration yields a [name, value] pair:
let formData = new FormData();
formData.append('key1', 'value1');
formData.append('key2', 'value2');
// List key/value pairs
for(let [name, value] of formData) {
alert(`${name} = ${value}`); // key1 = value1, then key2 = value2
}
The difference between append and set is easiest to feel by clicking. Both buttons below add the tag you type under the name "tag"; watch how append stacks values while set collapses them to one, and how getAll('tag') reflects the current state.
Sending a form with a file
Because the encoding is always multipart/form-data, files come along for free. An <input type="file"> field is serialized just like it would be in a native form submission.
<form id="formElem">
<input type="text" name="firstName" value="Maya">
Picture: <input type="file" name="picture" accept="image/*">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
let response = await fetch('/article/formdata/post/user-avatar', {
method: 'POST',
body: new FormData(formElem)
});
let result = await response.json();
alert(result.message);
};
</script>
Nothing extra is required. The moment you build new FormData(formElem), the chosen file’s bytes and its filename are pulled in with the text field, and the server receives a normal multipart upload.
Sending a form with Blob data
In the Fetch chapter you saw that binary data generated on the fly — say an image — can be sent as a Blob straight through fetch’s body.
That works, but often you want the image bundled inside a form, next to text fields like a name or other metadata. Servers also tend to be better set up to parse multipart forms than to handle a lone stream of raw bytes. Wrapping the Blob in FormData gives you both: structured fields and the binary payload in one request.
This example draws on a <canvas>, turns the drawing into a PNG Blob, and submits it as a form together with a text field:
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<input type="button" value="Submit" onclick="submit()">
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
async function submit() {
let imageBlob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
let formData = new FormData();
formData.append("firstName", "Maya");
formData.append("image", imageBlob, "image.png");
let response = await fetch('/article/formdata/post/image-form', {
method: 'POST',
body: formData
});
let result = await response.json();
alert(result.message);
}
</script>
</body>
The key line is how the Blob gets added:
formData.append("image", imageBlob, "image.png");
The three-argument form is what turns a raw Blob into a file field. It behaves exactly as if the form contained <input type="file" name="image"> and the visitor had picked a file named "image.png" (third argument) whose contents are imageBlob (second argument). The server reads it as an ordinary file upload — it has no idea the “file” was drawn in a canvas moments earlier.
Here is the whole flow minus the network hop. Draw with your mouse, then press Build FormData — the canvas is turned into a PNG Blob, wrapped with the three-argument append, and the demo reads the field back out so you can see it is now a real File with a name, type, and size.
Summary
FormData objects capture the contents of an HTML form so you can submit them with fetch or another network method. You can build one from an existing form with new FormData(form), or start empty and add fields yourself:
formData.append(name, value)formData.append(name, blob, fileName)formData.set(name, value)formData.set(name, blob, fileName)
Two things to keep straight:
setremoves any existing fields with the same name before adding, whileappendkeeps them and stacks duplicates. That is the only difference between the two.- To send a file, use the three-argument form. The last argument is the filename, which for a real
<input type="file">would come from the user’s filesystem.
The rest of the toolkit reads and edits fields:
formData.delete(name)formData.get(name)formData.has(name)
Build the object, hand it to fetch as body, and the browser takes care of the multipart encoding and boundary for you.