Skip to main content

How to Use FormData for Sending Form Data and Files in JavaScript

Introduction

HTML forms are everywhere on the web: login pages, registration forms, search bars, file uploads, contact forms, and checkout pages. When you need to capture all the data from a form and send it to a server, you could manually read every input field and assemble the data yourself. But there is a much better way: the FormData API.

FormData is a built-in browser object that captures all the fields in a form with a single line of code, handles file uploads natively, and formats everything in the multipart/form-data encoding that servers expect. It works seamlessly with fetch(), requiring no manual serialization or header configuration.

Beyond capturing existing forms, FormData also lets you build request bodies programmatically, adding fields, files, and blobs one by one. This makes it the ideal tool for any situation where you need to send structured data or binary content to a server.

In this guide, you will learn how to create FormData from HTML forms and from scratch, how to manipulate its contents with the full set of methods, how to send files and images, and how to pair it with fetch() for clean, modern form submissions.

Creating FormData from a Form

The most common use case is capturing all the data from an existing HTML form. You pass the form element to the FormData constructor, and it automatically collects every named field.

From an HTML Form Element

<form id="registrationForm">
<input type="text" name="username" value="john_doe">
<input type="email" name="email" value="john@example.com">
<input type="password" name="password" value="secret123">
<input type="number" name="age" value="28">
<select name="role">
<option value="user" selected>User</option>
<option value="admin">Admin</option>
</select>
<textarea name="bio">Hello, I'm John.</textarea>
<input type="checkbox" name="newsletter" value="yes" checked>
<button type="submit">Register</button>
</form>

<script>
const form = document.getElementById('registrationForm');

form.addEventListener('submit', (event) => {
event.preventDefault();

// One line captures ALL form fields
const formData = new FormData(form);

// Let's see what was captured
for (const [key, value] of formData) {
console.log(`${key}: ${value}`);
}
});
</script>

Output when submitted:

username: john_doe
email: john@example.com
password: secret123
age: 28
role: user
bio: Hello, I'm John.
newsletter: yes

Every input with a name attribute is automatically included. The FormData constructor reads the current values at the moment of creation, so it always reflects what the user has typed or selected.

What Gets Captured (and What Does Not)

FormData captures fields based on specific rules:

<form id="demoForm">
<!-- CAPTURED: has name and value -->
<input type="text" name="captured" value="yes">

<!-- NOT captured: no name attribute -->
<input type="text" value="invisible">

<!-- NOT captured: disabled fields -->
<input type="text" name="disabled_field" value="nope" disabled>

<!-- CAPTURED only when checked -->
<input type="checkbox" name="agree" value="true">

<!-- CAPTURED: the selected radio button's value -->
<input type="radio" name="color" value="red">
<input type="radio" name="color" value="blue" checked>

<!-- CAPTURED: file input (contains File objects) -->
<input type="file" name="avatar">

<!-- CAPTURED: select with selected option -->
<select name="country">
<option value="us">US</option>
<option value="uk" selected>UK</option>
</select>

<!-- CAPTURED: multiple select (multiple entries with same name) -->
<select name="skills" multiple>
<option value="js" selected>JavaScript</option>
<option value="py" selected>Python</option>
<option value="go">Go</option>
</select>
</form>

<script>
const formData = new FormData(document.getElementById('demoForm'));

for (const [key, value] of formData) {
console.log(`${key}: ${value}`);
}
</script>

Output:

captured: yes
color: blue
avatar: [File object or empty]
country: uk
skills: js
skills: py

Key rules to remember:

  • Fields must have a name attribute to be included
  • Disabled fields are excluded
  • Unchecked checkboxes and radio buttons are excluded
  • File inputs produce File objects (or empty File if no file is selected)
  • Multiple selections create multiple entries with the same key
note

The submit button's value is not automatically included by FormData. If you need to know which button was clicked (for forms with multiple submit buttons), you must add it manually. The submitter parameter of the FormData constructor handles this:

form.addEventListener('submit', (event) => {
event.preventDefault();
// Pass the submitter button as second argument
const formData = new FormData(form, event.submitter);
});

Creating FormData Programmatically

You do not always have an HTML form to work with. You can create FormData from scratch and add fields manually:

const formData = new FormData();

formData.append('username', 'jane_doe');
formData.append('email', 'jane@example.com');
formData.append('age', '25');

for (const [key, value] of formData) {
console.log(`${key}: ${value}`);
}

Output:

username: jane_doe
email: jane@example.com
age: 25

This is useful when:

  • You are building a request body from application state (not a visible form)
  • You are combining data from multiple sources
  • You need to upload a file generated in JavaScript (like a canvas image or a dynamically created blob)
  • You are sending data gathered from a custom UI component

Combining Form Capture with Manual Additions

A powerful pattern is capturing a form and then adding extra fields:

const form = document.getElementById('profileForm');
const formData = new FormData(form);

// Add fields that aren't in the form
formData.append('timestamp', Date.now().toString());
formData.append('clientVersion', '2.4.1');
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);

FormData Methods: append, set, delete, get, has

FormData provides a full set of methods for reading, adding, modifying, and removing entries.

append(name, value): Adding Entries

append() adds a new entry. If an entry with the same name already exists, append() adds another entry with that name (it does not overwrite):

const formData = new FormData();

formData.append('hobby', 'reading');
formData.append('hobby', 'cycling');
formData.append('hobby', 'cooking');

// All three entries exist
for (const [key, value] of formData) {
console.log(`${key}: ${value}`);
}

Output:

hobby: reading
hobby: cycling
hobby: cooking

This behavior is essential for representing multi-value fields like <select multiple>, checkboxes with the same name, or repeated form fields.

append() also accepts a third argument for file uploads (the filename):

// append(name, value)
formData.append('field', 'text value');

// append(name, blob, filename)
formData.append('avatar', imageBlob, 'profile.jpg');

set(name, value): Setting (Replacing) Entries

set() works like append(), but if entries with the same name already exist, set() removes all of them and adds a single new entry:

const formData = new FormData();

formData.append('color', 'red');
formData.append('color', 'blue');
formData.append('color', 'green');

console.log(formData.getAll('color'));
// ["red", "blue", "green"]

formData.set('color', 'purple');

console.log(formData.getAll('color'));
// ["purple"] (all previous entries replaced)

When to use append() vs. set():

const formData = new FormData();

// Use append when you want MULTIPLE values for the same key
formData.append('tags', 'javascript');
formData.append('tags', 'web');
formData.append('tags', 'frontend');
// tags has 3 entries

// Use set when you want exactly ONE value for a key
formData.set('title', 'First Draft');
formData.set('title', 'Final Version');
// title has only 1 entry: "Final Version"

get(name) and getAll(name): Reading Entries

get() returns the first value for a given name. getAll() returns an array of all values:

const formData = new FormData();
formData.append('skill', 'JavaScript');
formData.append('skill', 'Python');
formData.append('skill', 'Rust');
formData.append('name', 'Alice');

console.log(formData.get('skill'));
// "JavaScript" (only the first)

console.log(formData.getAll('skill'));
// ["JavaScript", "Python", "Rust"] (all of them)

console.log(formData.get('name'));
// "Alice"

console.log(formData.get('nonexistent'));
// null

has(name): Checking Existence

has() returns true if at least one entry with the given name exists:

const formData = new FormData();
formData.append('email', 'alice@example.com');

console.log(formData.has('email')); // true
console.log(formData.has('phone')); // false

A common use case is conditional logic before sending:

const formData = new FormData(form);

if (!formData.has('email') || !formData.get('email').trim()) {
alert('Email is required');
return;
}

delete(name): Removing Entries

delete() removes all entries with the given name:

const formData = new FormData();
formData.append('temp', 'will be removed');
formData.append('temp', 'also removed');
formData.append('keep', 'stays');

console.log(formData.has('temp')); // true

formData.delete('temp');

console.log(formData.has('temp')); // false
console.log(formData.get('keep')); // "stays"

Iterating Over FormData

FormData is iterable and supports several iteration patterns:

const formData = new FormData();
formData.append('name', 'Alice');
formData.append('age', '30');
formData.append('city', 'Paris');

// for...of (entries by default)
for (const [key, value] of formData) {
console.log(`${key} = ${value}`);
}

// .entries() (same as default iteration)
for (const [key, value] of formData.entries()) {
console.log(`${key} = ${value}`);
}

// .keys() (only the keys)
for (const key of formData.keys()) {
console.log(key); // "name", "age", "city"
}

// .values() (only the values)
for (const value of formData.values()) {
console.log(value); // "Alice", "30", "Paris"
}

// forEach
formData.forEach((value, key) => {
console.log(`${key}: ${value}`);
});

Converting FormData to a Plain Object

Sometimes you need a plain JavaScript object instead of FormData:

const formData = new FormData(form);

// Simple conversion (loses multiple values for the same key)
const simpleObject = Object.fromEntries(formData);
console.log(simpleObject);
// { name: "Alice", age: "30", hobby: "reading" }
// If there were multiple "hobby" entries, only the last one survives

// Safe conversion that preserves multiple values
function formDataToObject(formData) {
const obj = {};

for (const [key, value] of formData) {
if (obj.hasOwnProperty(key)) {
// Convert to array if not already
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key]];
}
obj[key].push(value);
} else {
obj[key] = value;
}
}

return obj;
}

const formData2 = new FormData();
formData2.append('name', 'Alice');
formData2.append('hobby', 'reading');
formData2.append('hobby', 'cycling');

console.log(formDataToObject(formData2));
// { name: "Alice", hobby: ["reading", "cycling"] }
caution

FormData stores all values as strings or File/Blob objects. Even if an input has type="number", the value in FormData is a string. If you need numeric values, you must convert them yourself:

const formData = new FormData(form);
const age = formData.get('age');

console.log(typeof age); // "string"
console.log(age); // "28"

const ageNumber = Number(age);
console.log(typeof ageNumber); // "number"

Sending Files with FormData

One of the biggest strengths of FormData is its native support for file uploads. It handles the complex multipart/form-data encoding automatically, which is required for sending binary data alongside text fields.

Uploading from a File Input

<form id="uploadForm">
<input type="text" name="description" placeholder="File description">
<input type="file" name="document" id="fileInput">
<button type="submit">Upload</button>
</form>

<script>
document.getElementById('uploadForm').addEventListener('submit', async (event) => {
event.preventDefault();

const formData = new FormData(event.target);

// The file input is automatically captured as a File object
const file = formData.get('document');
console.log(file);
// File { name: "report.pdf", size: 245891, type: "application/pdf", ... }

console.log(file instanceof File); // true
console.log(file.name); // "report.pdf"
console.log(file.size); // 245891
console.log(file.type); // "application/pdf"

const response = await fetch('/api/upload', {
method: 'POST',
body: formData
// Do NOT set Content-Type header (explained below)
});

const result = await response.json();
console.log('Upload result:', result);
});
</script>

Uploading Multiple Files

With a multiple file input, each file becomes a separate entry:

<input type="file" name="photos" multiple>

<script>
const fileInput = document.querySelector('input[type="file"]');

fileInput.addEventListener('change', () => {
const formData = new FormData();

// Each file from the input
for (const file of fileInput.files) {
formData.append('photos', file);
console.log(`Added: ${file.name} (${file.size} bytes)`);
}

// formData now has multiple "photos" entries
console.log(formData.getAll('photos'));
// [File, File, File, ...]
});
</script>

Uploading a Programmatically Created File

You can create files from JavaScript data and upload them:

// Create a text file from a string
const textContent = 'Hello, this is a generated file.';
const textBlob = new Blob([textContent], { type: 'text/plain' });

const formData = new FormData();
formData.append('file', textBlob, 'generated.txt');
// The third argument sets the filename

// Create a JSON file
const jsonData = { users: [{ name: 'Alice' }, { name: 'Bob' }] };
const jsonBlob = new Blob(
[JSON.stringify(jsonData, null, 2)],
{ type: 'application/json' }
);
formData.append('config', jsonBlob, 'data.json');

Uploading a Canvas Image

A very common use case is saving a canvas drawing or a cropped image:

const canvas = document.getElementById('myCanvas');

canvas.toBlob(async (blob) => {
const formData = new FormData();
formData.append('image', blob, 'canvas-drawing.png');
formData.append('title', 'My Drawing');

const response = await fetch('/api/upload-image', {
method: 'POST',
body: formData
});

const result = await response.json();
console.log('Image uploaded:', result);
}, 'image/png');

Or using the Promise-based approach:

function canvasToBlob(canvas, type = 'image/png', quality = 0.92) {
return new Promise((resolve) => {
canvas.toBlob(resolve, type, quality);
});
}

async function uploadCanvas(canvas) {
const blob = await canvasToBlob(canvas, 'image/jpeg', 0.85);

const formData = new FormData();
formData.append('photo', blob, 'photo.jpg');

return fetch('/api/photos', {
method: 'POST',
body: formData
});
}

File Validation Before Upload

Always validate files on the client side before uploading:

function validateFile(file, options = {}) {
const {
maxSizeMB = 10,
allowedTypes = [],
allowedExtensions = []
} = options;

const errors = [];

// Check file size
const maxBytes = maxSizeMB * 1024 * 1024;
if (file.size > maxBytes) {
errors.push(`File too large: ${(file.size / 1024 / 1024).toFixed(1)}MB (max ${maxSizeMB}MB)`);
}

// Check MIME type
if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
errors.push(`Invalid file type: ${file.type}. Allowed: ${allowedTypes.join(', ')}`);
}

// Check extension
if (allowedExtensions.length > 0) {
const ext = file.name.split('.').pop().toLowerCase();
if (!allowedExtensions.includes(ext)) {
errors.push(`Invalid extension: .${ext}. Allowed: ${allowedExtensions.join(', ')}`);
}
}

return { valid: errors.length === 0, errors };
}

// Usage
const fileInput = document.querySelector('input[type="file"]');

fileInput.addEventListener('change', () => {
const file = fileInput.files[0];
if (!file) return;

const validation = validateFile(file, {
maxSizeMB: 5,
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
allowedExtensions: ['jpg', 'jpeg', 'png', 'webp']
});

if (!validation.valid) {
alert('Upload error:\n' + validation.errors.join('\n'));
fileInput.value = ''; // Clear the input
return;
}

console.log('File is valid, ready to upload');
});

Sending FormData with Fetch

FormData is designed to work seamlessly with fetch(). However, there is one critical rule that catches many developers off guard.

Basic Send

const formData = new FormData(document.getElementById('myForm'));

const response = await fetch('/api/submit', {
method: 'POST',
body: formData
});

const result = await response.json();
console.log(result);

The Critical Rule: Do NOT Set Content-Type Manually

When sending FormData, you must let the browser set the Content-Type header automatically. The browser needs to generate a unique boundary string that separates each part of the multipart request, and it includes this boundary in the Content-Type header.

// WRONG: Setting Content-Type manually breaks the request
const response = await fetch('/api/upload', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data' // DO NOT DO THIS
},
body: formData
});
// Server receives corrupted data because the boundary is missing!
// CORRECT: Let the browser handle Content-Type
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
// No Content-Type header: browser sets it automatically
});
// Browser sets: Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
caution

This is the single most common mistake when using FormData with fetch(). If you set Content-Type: 'multipart/form-data' manually, the boundary string is missing, and the server cannot parse the multipart body. The browser must generate and include the boundary itself, which it only does when you leave the header unset.

If you have a helper function or API wrapper that sets Content-Type: 'application/json' by default, make sure to remove or override it when sending FormData.

What the Browser Actually Sends

When you send FormData, the browser constructs a multipart/form-data request body. Here is what the raw HTTP request looks like:

POST /api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123

------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="username"

john_doe
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg

[binary image data]
------WebKitFormBoundaryABC123--

Each field is separated by the boundary string. Text fields contain their value as plain text. File fields include the filename, content type, and the binary data.

Sending FormData with Authentication

If your API requires authentication headers, you can add those while still letting the browser handle Content-Type:

async function uploadWithAuth(formData, authToken) {
const response = await fetch('/api/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
// No Content-Type here: browser handles it for FormData
},
body: formData
});

if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}

return response.json();
}

Adapting a Fetch Wrapper for FormData

If you have a reusable fetch wrapper, it needs to detect FormData and avoid setting Content-Type:

async function apiFetch(endpoint, options = {}) {
const baseURL = 'https://api.example.com';

const headers = {
'Authorization': `Bearer ${getToken()}`
};

// Only set Content-Type for non-FormData bodies
if (!(options.body instanceof FormData)) {
headers['Content-Type'] = 'application/json';
headers['Accept'] = 'application/json';

// Auto-stringify objects
if (options.body && typeof options.body === 'object') {
options.body = JSON.stringify(options.body);
}
}

const response = await fetch(`${baseURL}${endpoint}`, {
...options,
headers: {
...headers,
...options.headers
}
});

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}

return response.json();
}

// Sending JSON: Content-Type is set automatically
await apiFetch('/users', {
method: 'POST',
body: { name: 'Alice', email: 'alice@example.com' }
});

// Sending FormData: Content-Type is NOT set (browser handles it)
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
formData.append('name', 'Alice');

await apiFetch('/users/avatar', {
method: 'POST',
body: formData
});

FormData vs. JSON: When to Use Which

CriteriaFormDataJSON
File uploadsNative supportNot possible (Base64 workaround is inefficient)
Binary dataNative supportRequires encoding
Content-Typemultipart/form-data (auto)application/json (manual)
Data structureFlat key-value pairsNested objects, arrays, any structure
Server framework supportUniversalUniversal
Typical useForms with files, mixed dataPure data APIs, structured payloads

Use FormData when:

  • The form includes file uploads
  • You are capturing an HTML form's contents
  • You need to mix text fields and binary data in one request

Use JSON when:

  • You are sending structured data without files
  • The data has nested objects or complex types
  • The API explicitly expects JSON

Practical Examples

Complete Form Submission with Progress Feedback

<form id="profileForm">
<input type="text" name="displayName" placeholder="Display Name" required>
<input type="email" name="email" placeholder="Email" required>
<textarea name="bio" placeholder="Bio"></textarea>
<input type="file" name="avatar" accept="image/*">
<button type="submit" id="submitBtn">Save Profile</button>
<p id="status"></p>
</form>

<script>
const form = document.getElementById('profileForm');
const submitBtn = document.getElementById('submitBtn');
const status = document.getElementById('status');

form.addEventListener('submit', async (event) => {
event.preventDefault();

const formData = new FormData(form);

// Client-side validation
if (!formData.get('displayName').trim()) {
status.textContent = 'Display name is required';
return;
}

// Check avatar file if provided
const avatar = formData.get('avatar');
if (avatar && avatar.size > 0) {
if (avatar.size > 2 * 1024 * 1024) {
status.textContent = 'Avatar must be under 2MB';
return;
}
if (!avatar.type.startsWith('image/')) {
status.textContent = 'Avatar must be an image';
return;
}
} else {
// Remove empty file field if no file was selected
formData.delete('avatar');
}

// Add metadata
formData.append('updatedAt', new Date().toISOString());

submitBtn.disabled = true;
status.textContent = 'Saving...';

try {
const response = await fetch('/api/profile', {
method: 'PUT',
body: formData
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `Server error: ${response.status}`);
}

const result = await response.json();
status.textContent = 'Profile saved successfully!';
status.style.color = 'green';
} catch (error) {
status.textContent = `Error: ${error.message}`;
status.style.color = 'red';
} finally {
submitBtn.disabled = false;
}
});
</script>

Drag-and-Drop File Upload

<div id="dropZone" style="
width: 400px;
height: 200px;
border: 2px dashed #bdc3c7;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui;
color: #7f8c8d;
transition: border-color 0.2s, background 0.2s;
">
Drop files here or click to browse
<input type="file" id="hiddenInput" multiple style="display: none">
</div>
<ul id="fileList"></ul>

<script>
const dropZone = document.getElementById('dropZone');
const hiddenInput = document.getElementById('hiddenInput');
const fileList = document.getElementById('fileList');

// Click to browse
dropZone.addEventListener('click', () => hiddenInput.click());

// Visual feedback for drag
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#3498db';
dropZone.style.background = '#ebf5fb';
});

dropZone.addEventListener('dragleave', () => {
dropZone.style.borderColor = '#bdc3c7';
dropZone.style.background = 'transparent';
});

// Handle drop
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#bdc3c7';
dropZone.style.background = 'transparent';

uploadFiles(e.dataTransfer.files);
});

// Handle file input change
hiddenInput.addEventListener('change', () => {
uploadFiles(hiddenInput.files);
});

async function uploadFiles(files) {
if (files.length === 0) return;

const formData = new FormData();

for (const file of files) {
formData.append('files', file);

const li = document.createElement('li');
li.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} KB) - uploading...`;
li.id = `file-${file.name}`;
fileList.appendChild(li);
}

try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});

if (!response.ok) throw new Error(`Upload failed: ${response.status}`);

const result = await response.json();

// Update UI
for (const file of files) {
const li = document.getElementById(`file-${file.name}`);
if (li) li.textContent = `${file.name} - uploaded successfully`;
}
} catch (error) {
for (const file of files) {
const li = document.getElementById(`file-${file.name}`);
if (li) li.textContent = `${file.name} - failed: ${error.message}`;
}
}
}
</script>

Sending FormData as JSON (When the Server Requires It)

Sometimes you start with a form but the API expects JSON, not multipart data. You can convert FormData to JSON:

const form = document.getElementById('myForm');

form.addEventListener('submit', async (event) => {
event.preventDefault();

const formData = new FormData(form);

// Convert to a plain object
const data = Object.fromEntries(formData);

// Send as JSON instead of FormData
const response = await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});

const result = await response.json();
console.log(result);
});
note

This conversion loses file data (files become "[object File]" strings) and collapses multiple values for the same key into just the last one. Only use this pattern for simple text-only forms. For files or multi-value fields, send the FormData directly.

Modifying Form Data Before Sending

A common pattern is intercepting a form submission, modifying the data, and then sending it:

form.addEventListener('submit', async (event) => {
event.preventDefault();

const formData = new FormData(form);

// Remove sensitive data you don't want to send
formData.delete('creditCard');

// Transform values
const email = formData.get('email');
formData.set('email', email.toLowerCase().trim());

// Add computed fields
const firstName = formData.get('firstName');
const lastName = formData.get('lastName');
formData.append('fullName', `${firstName} ${lastName}`);

// Remove empty optional fields
for (const [key, value] of [...formData.entries()]) {
if (typeof value === 'string' && !value.trim()) {
formData.delete(key);
}
}

const response = await fetch('/api/register', {
method: 'POST',
body: formData
});
});
tip

When iterating over FormData and deleting entries at the same time, use the spread operator to snapshot the entries first ([...formData.entries()]). Modifying a collection while iterating over it directly can lead to skipped entries.

Debugging FormData Contents

FormData does not have a simple way to log its contents. You cannot just console.log(formData) and see the fields. Here are ways to inspect it:

const formData = new FormData(form);

// Method 1: Iterate and log
for (const [key, value] of formData) {
if (value instanceof File) {
console.log(`${key}: [File] ${value.name} (${value.size} bytes, ${value.type})`);
} else {
console.log(`${key}: ${value}`);
}
}

// Method 2: Convert to object for quick inspection
console.log(Object.fromEntries(formData));
// Warning: loses duplicate keys and file details

// Method 3: Convert to detailed array
const entries = [...formData.entries()].map(([key, value]) => {
if (value instanceof File) {
return { key, type: 'File', name: value.name, size: value.size };
}
return { key, type: 'string', value };
});
console.table(entries);

Summary

FormData is a built-in API for collecting, manipulating, and sending form data and files in JavaScript. It captures all named fields from an HTML form with new FormData(form), or you can build it programmatically with append() and set().

The key methods are append() (adds an entry, allows duplicates), set() (replaces all entries with the same name), get() and getAll() (read one or all values), has() (check existence), and delete() (remove all entries for a key). FormData is iterable, so you can loop through it with for...of, .entries(), .keys(), .values(), or .forEach().

For file uploads, FormData handles the complex multipart/form-data encoding automatically. You can capture files from <input type="file">, create files from Blobs or Canvas elements, and mix text fields with binary data in a single request.

When sending FormData with fetch(), the most important rule is to never set the Content-Type header manually. The browser must generate the multipart boundary string and include it in the header. Setting it yourself strips the boundary and corrupts the request. Add any other headers (like Authorization) freely, just leave Content-Type alone.

Choose FormData over JSON when your request includes file uploads or when you are capturing an HTML form. Choose JSON when sending structured data without binary content.