JavaScript FormData Table
| Piece | What it does | Field note |
|---|---|---|
new FormData(form) | Harvest a whole form | Every named input becomes an entry - the no-hand-copying constructor |
fd.append(k, v) | Add an entry | Multiple same-key entries are legal - getAll() reads them back |
fd.append(k, file) | Attach a FILE | The file input's files[0] drops straight in - uploads in one line |
fd.get / getAll | Read entries back | get returns FIRST match; getAll returns the array |
fetch(url, { method:'POST', body: fd }) | Ship it | Do NOT set Content-Type - the browser writes multipart boundary itself |
fd.delete / set | Trim before send | set replaces all entries for the key - normalize before upload |
for (const [k, v] of fd) | It is iterable | Inspect before send - the debug loop |
JSON vs FormData | Two upload dialects | JSON = string body + application/json; FormData = multipart + files |
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
- Harvest, don't hand-copy: const fd = new FormData(document.querySelector('form')) - every named input becomes an entry, including files.
- Upload with fetch: fetch('/upload', { method: 'POST', body: fd }) - no headers object; the browser writes Content-Type with the boundary.
- 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.