FormData is a JavaScript interface that stores a list of form fields as name and value entries, where a value is a string or a File. It is defined in the WHATWG XMLHttpRequest Standard and is the usual way to upload files with fetch. When it is used as a request body, it is serialized as multipart/form-data, not as a query-style string.
You create an empty object with new FormData() or fill one from a form element with new FormData(formElement). The entry list keeps insertion order and allows duplicate names. The methods are:
Strings are stored as strings. A Blob value is converted to a File, and append(name, blob, filename) lets you choose the file name. Without a filename, a plain Blob gets the file name "blob".
When you hand a FormData to fetch as body, the request uses the Content-Type multipart/form-data; boundary=..., where the boundary string is generated for you. Each entry becomes a part with its own Content-Disposition header, and file parts also carry their own Content-Type.
const fd = new FormData();
fd.append('tag', 'a'); fd.append('tag', 'b');
fd.set('name', 'Ada');
fd.append('doc', new Blob(['hello'], { type: 'text/plain' }), 'hi.txt');
console.log(fd.get('tag'), fd.getAll('tag'), fd.has('nope'), fd.get('nope'));
const f = fd.get('doc');
console.log(f instanceof File, f.name, f.size, f.type);
fd.set('tag', 'only');
console.log([...fd.keys()]);
Output from Node 22.22.0:
a [ 'a', 'b' ] false null
true hi.txt 5 text/plain
[ 'tag', 'name', 'doc' ]
On the wire, the same object becomes:
Content-Disposition: form-data; name="doc"; filename="hi.txt"
Content-Type: text/plain
hello
Pass the object as the body and do not set a Content-Type header: fetch(url, { method: 'POST', body: fd }). The browser adds the multipart header with the boundary. To send JSON from the same data, use JSON.stringify(Object.fromEntries(fd)), though that keeps only the last value of any repeated name.