JavaScript FormData Table

PieceWhat it doesField note
new FormData(form)Harvest a whole formEvery named input becomes an entry - the no-hand-copying constructor
fd.append(k, v)Add an entryMultiple same-key entries are legal - getAll() reads them back
fd.append(k, file)Attach a FILEThe file input's files[0] drops straight in - uploads in one line
fd.get / getAllRead entries backget returns FIRST match; getAll returns the array
fetch(url, { method:'POST', body: fd })Ship itDo NOT set Content-Type - the browser writes multipart boundary itself
fd.delete / setTrim before sendset replaces all entries for the key - normalize before upload
for (const [k, v] of fd)It is iterableInspect before send - the debug loop
JSON vs FormDataTwo upload dialectsJSON = string body + application/json; FormData = multipart + files
Reference: the MDN FormData reference. FormData is the upload dialect: new FormData(form) harvests every named input, file inputs drop their File objects straight in, and fetch ships it as multipart with the boundary header WRITTEN BY THE BROWSER - which is why manually setting Content-Type on a FormData request breaks the upload (your static header has no boundary). Bottom line: the constructor beats hand-copying fields, append carries files natively, getAll reads repeated keys, and the JSON-vs-FormData choice is really strings-versus-files. Related tools: form elements table (where the names come from), fetch table (the request side), and input types table (the file input's accept and multiple).

FormData is the upload dialect of fetch: new FormData(form) harvests every named input from a form in one constructor call, file inputs drop their File objects in natively, and fetch ships the whole thing as multipart/form-data with the boundary header written by the browser.

Bottom line: never set Content-Type on a FormData request - the browser generates the multipart boundary itself, and your manually-set static header (missing the boundary) breaks the upload server-side. The JSON-versus-FormData choice is really strings-versus-files: JSON bodies for structured data, FormData when files ride along.

The honest part: FormData is not a plain object - it is iterable (for...of works), keys can repeat (getAll returns the array, get returns the first), and it stringifies to '{}' if you try JSON.stringify on it. Inspect it with its own API or the debug loop, not with object tools.

How to use

  1. Harvest, don't hand-copy: const fd = new FormData(document.querySelector('form')) - every named input becomes an entry, including files.
  2. Upload with fetch: fetch('/upload', { method: 'POST', body: fd }) - no headers object; the browser writes Content-Type with the boundary.
  3. Trim before sending: fd.delete('internalField'); fd.set('slug', slugify(fd.get('title'))) - normalize on the way out, not in the DOM.

Frequently asked questions

Why did my FormData upload break when I set Content-Type manually?

Because multipart/form-data needs a BOUNDARY - a generated delimiter string separating the parts - and the browser writes it into the Content-Type header it attaches automatically. Your manually-set Content-Type: multipart/form-data has no boundary parameter, so the server cannot find where one field ends and the next begins and rejects or misparses the body. The fix is to pass the FormData as the fetch body and touch no headers at all: the browser's auto-generated header carries the exact boundary matching the encoded body.

How do file uploads actually work with FormData?

Directly - a File object (from an input type=file's .files[0] or a drag event's dataTransfer) is a valid append value: fd.append('avatar', file) stores filename, MIME type and bytes. Multiple files either append under one key (avatar, then getAll('avatar')) or append with the array syntax. The server receives standard multipart parts with filenames. What FormData does NOT do: progress (use XMLHttpRequest.upload.onprogress or fetch streams), or chunking (that is a slicing loop). For most uploads the whole flow is three lines: harvest, append file, fetch.

When is FormData the wrong choice versus a JSON body?

When no files are involved and the API prefers structured types. JSON preserves types (numbers stay numbers, nesting stays nested) and is the default dialect of most modern APIs; FormData flattens everything to strings plus files - sending nested objects means JSON.stringify-ing them INTO a field and parsing server-side. The decision tree: files present = FormData (possibly mixed: a JSON metadata string field plus file fields); pure structured data = JSON with Content-Type: application/json. Mixed payloads are legitimate and common - one field carrying metadata JSON, the rest files.

Why does JSON.stringify(fd) return an empty object?

FormData is not a plain object - it has no enumerable own properties for JSON.stringify to see, so you get '{}' regardless of contents. It is an iterable container with its own API: fd.get(key), fd.getAll(key) (repeated keys), fd.has, fd.entries(). The inspection loop is for (const [key, value] of fd.entries()) console.log(key, value) - and note value is a File for file fields, which prints as an object. The same trap hits spreading: {...fd} is empty; Array.from(fd) or Object.fromEntries(fd) works because they consume the iterator.

Related tools