FormData in 60 seconds
FormData is the browser's built-in representation of a form submission — the same encoding a plain HTML <form> would produce, but under JavaScript control. Create it from a form element and every named, enabled field is captured:
<form id="contact">
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message"></textarea>
<input name="attachment" type="file" />
<button>Send</button>
</form>
<script>
const form = document.getElementById("contact");
form.addEventListener("submit", async (e) => {
e.preventDefault(); // stop the full-page reload
const data = new FormData(form); // captures all named fields + files
const res = await fetch("https://splitforms.com/api/submit", {
method: "POST",
body: data, // browser sets multipart + boundary
});
if (res.ok) form.reset(); // show your success UI here
});
</script>That's the whole pattern: no page reload, no serialization code, files included. The AJAX pattern in depth — loading states, error UI, progressive enhancement — is covered in the AJAX contact form guide.
Reading and modifying values
const data = new FormData(form);
data.get("email"); // single value (first if multiple)
data.getAll("tags"); // array — for checkboxes / multi-select
data.has("newsletter"); // checkbox present?
data.set("name", "Jane"); // overwrite (single value)
data.append("tags", "vip"); // add another value
data.delete("message"); // remove a field
for (const [key, value] of data.entries()) {
console.log(key, value); // files log as File objects
}Two gotchas matter in real code. Checkboxes and radio groups: unchecked boxes submit nothing (by HTML design), so has() is the test — and a checked box sends its value attribute (default "on"). Multi-value fields: Object.fromEntries(data) silently keeps only the last value; use getAll() instead.
The golden rule: never set Content-Type yourself
// ❌ Breaks the request — boundary goes missing:
fetch(url, {
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
body: new FormData(form),
});
// ✅ Correct — the browser adds:
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
fetch(url, { method: "POST", body: new FormData(form) });Multipart bodies split fields with a random boundary string declared in the header. When you hard-code the header without a boundary, the server can't parse the body — symptoms range from empty submissions to 400/422 errors. Letting fetch set it is not optional; it's the mechanism.
FormData vs JSON: which should you send?
- Files involved → FormData (multipart). JSON can't carry binary without base64 bloat.
- Mirroring classic HTML behavior → FormData or
application/x-www-form-urlencoded; endpoints built for HTML forms expect it. - Nested/structured payloads, no files → JSON is cleaner:
body: JSON.stringify(Object.fromEntries(data))withContent-Type: application/json.
// Same form, JSON flavor (no files):
const res = await fetch("https://splitforms.com/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});splitforms accepts all three encodings on one endpoint — multipart/form-data, application/x-www-form-urlencoded, and application/json — with proper CORS headers, so the same access key works from a plain HTML form, a fetch() call, or a server action. Details in the API reference.
The gotchas that break real submissions
- Missing
nameattributes. FormData mirrors HTML submission rules: noname, no field. IDs are not submitted. - Disabled inputs are skipped. Use
readonlyfor display-only values you still want submitted. - Capturing too early. Build the FormData inside the submit handler, not at page load.
- Logging the object.
console.log(data)shows an emptyFormData— log[...data.entries()]. - Files need real multipart. Don't mix files into JSON payloads; keep them in FormData. Check endpoint limits (splitforms: 5 files × 5 MB per submission with Storage connected — see the file upload guide).
- UTM/hidden metadata. Append tracking fields programmatically:
data.append("utm_source", params.get("utm_source") ?? "")— pattern in tracking form submission sources.
FAQ
What is FormData in JavaScript?
FormData is a built-in browser API that captures the fields of an HTML form (or fields you add programmatically) as key/value pairs, ready to send over HTTP. new FormData(form) reads every named input — including file inputs — and a fetch() call with that FormData as the body automatically sends a correctly-encoded multipart/form-data request.
How do I send FormData with fetch?
Pass it directly as the body with method POST and do NOT set a Content-Type header — the browser sets multipart/form-data with the correct boundary automatically: const res = await fetch(url, { method: 'POST', body: new FormData(form) }). Setting Content-Type manually breaks the boundary and is the most common FormData bug.
Should I send FormData or JSON?
Send FormData (multipart) when the payload includes files or must match classic HTML form behavior. Send JSON (JSON.stringify with Content-Type: application/json) for structured API payloads without files. splitforms accepts both on the same endpoint — form-encoded, multipart, and JSON — so pick whichever fits your code.
Why is my FormData empty when I read it?
Four usual reasons: inputs lack a name attribute (FormData only captures named fields); the input is disabled (disabled fields are excluded, matching HTML submission rules); you built FormData before the user finished (capture it inside the submit handler); or you're console.logging the FormData object itself — log [...data.entries()] instead.
How do I append a file to FormData?
Either include a file input with a name and let new FormData(form) capture it, or append programmatically: data.append('attachment', fileInput.files[0]). The request must go as multipart/form-data — which happens automatically when the body is FormData. Check the endpoint's file limits (splitforms: 5 files per submission at 5 MB each with Storage connected).
How do I convert FormData to a plain object?
Object.fromEntries(new FormData(form)) works for single-value fields. Beware multi-value fields (checkbox groups, multiple selects) — fromEntries keeps only the last value; use data.getAll('fieldname') for those.
Point your fetch at https://splitforms.com/api/submit and every submission is delivered, stored, and spam-filtered. Get a free access key.
Related: send form data to email with fetch, submit forms with AJAX, and sending form data to an API.