splitforms.com
← Back to the journal

FormData in JavaScript: Submit Forms with fetch (2026 Guide)

The complete FormData guide: reading form fields, appending files, sending multipart and urlencoded payloads with fetch, and the gotchas that break…

Start free — 500 submissions/moSee pricing →No credit card. Paid plans from $5/mo.

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)) with Content-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

  1. Missing name attributes. FormData mirrors HTML submission rules: no name, no field. IDs are not submitted.
  2. Disabled inputs are skipped. Use readonly for display-only values you still want submitted.
  3. Capturing too early. Build the FormData inside the submit handler, not at page load.
  4. Logging the object. console.log(data) shows an empty FormData — log [...data.entries()].
  5. 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).
  6. 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.

Related articles

More practical guidance from tutorials.

Browse the journal →
Tutorials

Custom Auto-Responder Emails for HTML Forms (HTML + CSS, 2026)

Design fully branded auto-responder emails for your HTML forms: paste your own HTML/CSS temp

9 min readRead →
Tutorials

Send Form Emails From Your Own Domain (Custom SMTP, 2026)

Send form notification and auto-responder emails from your own domain with custom SMTP: encr

10 min readRead →
Tutorials

HTMX Contact Form Without a Backend (2026)

Build a working HTMX contact form with hx-post, inline success and error states, and zero ba

8 min readRead →

Building forms with ChatGPT, Claude, Cursor, or v0? Connect the native MCP server and give your agent a production form backend.

Explore the MCP server →

Give your form a production backend.

One endpoint adds delivery, spam filtering, storage, and integrations. Start with 500 submissions a month for free.

Create free accountRead the docs →
Secure checkoutSSL encryptionPrivacyProtected
VISAAMERICANEXPRESSstripe