Node.js 23 🟢 Working with FormData, Blob, File, and StructuredClone
Node.js has been converging on Web Platform APIs for several versions. fetch became stable in Node.js 21 and enabled by default in 22. Alongside it came FormData, Blob, and File—the classes that make HTTP requests and file handling work the same way in Node.js as they do in the browser. structuredClone completes the picture: a native deep-clone function that handles circular references and transferable objects correctly, replacing the JSON.parse(JSON.stringify(...)) hack that lost dates, maps, sets, and prototypes.
This chapter covers these four APIs together because they are designed to work together. FormData holds Blob and File objects, fetch sends them as multipart bodies, and structuredClone clones them across worker boundaries or stores them in IndexedDB. Understanding them as a family, rather than as isolated utilities, is the way they are meant to be used.
Key point: All four are Web Platform standards, implemented natively in Node.js. FormData constructs multipart/form-data bodies. Blob represents immutable binary data. File is a Blob with a name and a last-modified date. structuredClone deep-copies values using the structured clone algorithm, preserving types that JSON cannot represent.
Why these APIs exist in Node.js
The interoperability problem. Code that runs in both the browser and Node.js needs a single set of APIs for HTTP, file handling, and data transfer. Before these classes were native, Node.js developers used form-data (an npm package) for multipart uploads, Buffer for binary data, and JSON.parse(JSON.stringify()) for deep copies. Each had different semantics from the browser equivalents. Native Web APIs eliminate the divergence.
The multipart problem. Uploading files requires multipart/form-data encoding. The FormData class constructs this encoding. When passed to fetch as the body, fetch sets the Content-Type header to multipart/form-data with a boundary and serializes the fields and files accordingly . This is the same FormData that browsers use, which means the same upload code works in both environments.
The binary data problem. Node.js has Buffer, which is fast and mutable. But Buffer is not transferable between workers, not clonable by structuredClone, and not a standard type. Blob is immutable, clonable, transferable, and standard. It represents raw data with an optional MIME type. A Blob can be converted to a Buffer when needed, but it can also be sent through postMessage, stored in IndexedDB, or attached to a FormData without conversion .
The file problem. A File is a Blob with a name and lastModified property. It is what an <input type="file"> produces in the browser. In Node.js, File can be constructed from buffers or strings and attached to a FormData for upload. The fetch request then sends the file with its name and content type .
The deep-copy problem. JSON.parse(JSON.stringify(obj)) is a common deep-clone idiom, but it fails for Date, Map, Set, RegExp, ArrayBuffer, and circular references. It also converts undefined to null and loses the prototype chain. structuredClone uses the structured clone algorithm, which handles all of these correctly. It is the same algorithm used by postMessage and IndexedDB, so cloning a value produces the same result whether it goes through a worker boundary or not .
a. FormData: multipart form construction
FormData represents a set of key-value pairs, where values can be strings or Blob/File objects. It is primarily used as the body of a fetch request with multipart/form-data encoding.
const form = new FormData();
form.append('name', 'Alice');
form.append('email', 'alice@example.com');
const response = await fetch('https://api.example.com/users', {
method: 'POST',
body: form
});
When FormData is passed as fetch‘s body, fetch sets the Content-Type header automatically. It includes a boundary parameter that separates the fields in the serialized body. You do not set Content-Type manually; doing so would omit the boundary and break the encoding .
Appending a Blob or File attaches it as a file part:
const file = new File(['Hello, world'], 'greeting.txt', { type: 'text/plain' });
form.append('attachment', file);
The third argument to append can specify a filename for values that are not already File objects:
const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
form.append('document', blob, 'document.bin');
This creates a file part with the name document and the filename document.bin. Without the filename, a default name like blob is used .
FormData has methods for reading and modifying entries. get retrieves a value, set replaces a value, delete removes a value, and has checks for existence. entries, keys, values, and forEach provide iteration. The append method adds a new entry even if one with the same name exists, while set replaces all existing entries with the given name.
On the server side, request.formData() parses a multipart/form-data body into a FormData instance. This is how an upload endpoint reads the files and fields sent by a client .
b. Blob and File: immutable binary data
Blob (Binary Large Object) is an immutable container for raw data. It is created from an array of sources—strings, ArrayBuffers, TypedArrays, DataViews, or other Blobs—and an options object with a type property.
const blob = new Blob(['Hello', ' ', 'world'], { type: 'text/plain' });
console.log(blob.size); // 11
console.log(blob.type); // 'text/plain'
The data is copied into the Blob at construction. Modifying the source array afterward does not change the Blob. String sources are encoded as UTF-8.
Blob has methods for reading its content. text() returns a promise for the UTF-8 decoded string. arrayBuffer() returns a promise for the raw bytes. bytes() returns a promise for a Uint8Array . stream() returns a ReadableStream for incremental reading.
const text = await blob.text();
const buffer = await blob.arrayBuffer();
const bytes = await blob.bytes();
const stream = blob.stream();
Blob is transferable and clonable. It can be sent through MessagePort to multiple destinations without copying the underlying data. The data is copied only when a read method is called .
File extends Blob with two additional properties: name and lastModified. The constructor takes the parts, the filename, and an options object.
const file = new File(['contents'], 'notes.txt', {
type: 'text/plain',
lastModified: Date.now()
});
console.log(file.name); // 'notes.txt'
console.log(file.lastModified); // timestamp
File is used wherever a Blob is accepted. A FormData can hold File objects, and a fetch request can send them. The distinction is metadata: a Blob has a type but no name; a File has both.
c. structuredClone: deep cloning with the structured clone algorithm
structuredClone is a global function that creates a deep clone of a value using the structured clone algorithm. It handles types that JSON cannot represent and preserves circular references.
const original = {
date: new Date(),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
buffer: new Uint8Array([1, 2, 3])
};
const clone = structuredClone(original);
console.log(clone.date instanceof Date); // true
console.log(clone.map instanceof Map); // true
console.log(clone.set instanceof Set); // true
JSON serialization would convert Date to a string, Map and Set to empty objects, and Uint8Array to an object with numeric keys. structuredClone preserves the types .
Circular references are preserved correctly:
const original = { name: 'test' };
original.self = original;
const clone = structuredClone(original);
console.log(clone.self === clone); // true
The clone’s self reference points to the clone, not to the original. JSON.stringify throws on circular references.
The options parameter accepts a transfer array. Objects listed there are transferred rather than cloned. After the call, the original object is detached and unusable.
const buffer = new ArrayBuffer(1024);
const transferred = structuredClone({ data: buffer }, {
transfer: [buffer]
});
console.log(buffer.byteLength); // 0 (detached)
This is used to move ownership of a buffer to a worker without copying the underlying memory. The original buffer becomes zero-length .
Not everything can be cloned. Functions, DOM nodes, and class instances with methods throw a DataCloneError. Plain objects, arrays, Date, RegExp, Map, Set, ArrayBuffer, TypedArray, Blob, File, and FormData are cloneable. The error is thrown at the point of cloning, not silently ignored .
The structuredClone implementation is the same algorithm used by postMessage and IndexedDB. A value that survives structuredClone will survive a worker transfer. A value that throws on structuredClone will throw on postMessage. This consistency is the point: one mental model for all structured cloning.
Complete Example Session
// ============================================
// PART 1: BASIC FORMDATA
// ============================================
const form = new FormData();
form.append('name', 'Alice');
form.append('age', '30');
console.log(form.get('name')); // 'Alice'
console.log(form.has('age')); // true
// ============================================
// PART 2: FORMDATA WITH FILE
// ============================================
const file = new File(['Hello, world'], 'hello.txt', { type: 'text/plain' });
const form = new FormData();
form.append('document', file);
const response = await fetch('https://httpbin.org/post', {
method: 'POST',
body: form
});
// ============================================
// PART 3: FORMDATA FROM BLOB
// ============================================
const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
const form = new FormData();
form.append('data', blob, 'data.bin');
console.log(form.get('data').name); // 'data.bin'
// ============================================
// PART 4: BLOB CREATION AND READING
// ============================================
import { Blob } from 'node:buffer';
const blob = new Blob(['Hello', ' ', 'world'], { type: 'text/plain' });
console.log(blob.size); // 11
console.log(blob.type); // 'text/plain'
const text = await blob.text();
console.log(text); // 'Hello world'
// ============================================
// PART 5: BLOB METHODS
// ============================================
const blob = new Blob(['data']);
const buffer = await blob.arrayBuffer();
const bytes = await blob.bytes();
const stream = blob.stream();
console.log(buffer.byteLength); // 4
console.log(bytes.length); // 4
console.log(stream instanceof ReadableStream); // true
// ============================================
// PART 6: FILE METADATA
// ============================================
const file = new File(['content'], 'notes.txt', {
type: 'text/plain',
lastModified: Date.now()
});
console.log(file.name); // 'notes.txt'
console.log(file.lastModified); // timestamp
console.log(file.size); // 7
console.log(file.type); // 'text/plain'
// ============================================
// PART 7: STRUCTUREDCLONE WITH COMPLEX TYPES
// ============================================
const original = {
date: new Date('2026-01-01'),
map: new Map([['a', 1], ['b', 2]]),
set: new Set([1, 2, 3]),
regex: /pattern/gi,
buffer: new Uint8Array([1, 2, 3])
};
const clone = structuredClone(original);
console.log(clone.date instanceof Date); // true
console.log(clone.map instanceof Map); // true
console.log(clone.set instanceof Set); // true
console.log(clone.regex instanceof RegExp); // true
// ============================================
// PART 8: CIRCULAR REFERENCE
// ============================================
const original = { name: 'test' };
original.self = original;
const clone = structuredClone(original);
console.log(clone.self === clone); // true
console.log(clone !== original); // true
// ============================================
// PART 9: TRANSFER ARRAYBUFFER
// ============================================
const buffer = new ArrayBuffer(1024);
const transferred = structuredClone({ data: buffer }, {
transfer: [buffer]
});
console.log(buffer.byteLength); // 0 (detached)
console.log(transferred.data.byteLength); // 1024
// ============================================
// PART 10: CLONE ERROR
// ============================================
try {
structuredClone({ fn: () => {} });
} catch (error) {
console.log(error.name); // 'DataCloneError'
}
The ten parts covered basic FormData, FormData with File, FormData from Blob, Blob creation and reading, Blob methods, File metadata, structuredClone with complex types, circular references, transfer arrays, and clone errors.
Quick Reference
FormData Methods
| Method | Purpose |
|---|---|
append(name, value) | Add a new entry |
append(name, blob, filename) | Add a file with name |
get(name) | Get the first value |
set(name, value) | Replace all entries |
delete(name) | Remove entries |
has(name) | Check existence |
entries() | Iterate key-value pairs |
Blob and File
| Feature | Blob | File |
|---|---|---|
| Data | Immutable binary | Immutable binary |
| MIME type | type | type |
| Name | No | name |
| Last modified | No | lastModified |
| Transferable | Yes | Yes |
| Clonable | Yes | Yes |
| Read methods | text(), arrayBuffer(), bytes(), stream() | Inherited |
structuredClone
| Aspect | Behavior |
|---|---|
| Algorithm | Structured clone |
| Circular refs | Preserved |
| Date/Map/Set | Preserved |
| ArrayBuffer | Cloned or transferred |
| Functions | Throws DataCloneError |
| DOM nodes | Throws DataCloneError |
| Transfer option | Detaches originals |
Cloneable Types
| Type | structuredClone | JSON |
|---|---|---|
Date | Preserved | String |
Map | Preserved | {} |
Set | Preserved | {} |
RegExp | Preserved | {} |
Uint8Array | Preserved | {} |
| Circular refs | Preserved | Throws |
| Functions | Throws | Omitted |
Best Practices
✅ Do This:
// Let fetch set Content-Type for FormData
await fetch(url, { method: 'POST', body: form }); // ✅
// Use File for uploads with names
const file = new File(['data'], 'file.txt', { type: 'text/plain' }); // ✅
// Use structuredClone for deep copies
const clone = structuredClone(obj); // ✅
// Use transfer for large buffers
structuredClone(obj, { transfer: [buffer] }); // ✅
// Check instanceof File when reading formData
if (data.get('file') instanceof File) { } // ✅
// Use Blob for immutable binary data
const blob = new Blob([buffer], { type: 'application/octet-stream' }); // ✅
❌ Don’t Do This:
// Don't set Content-Type manually for FormData
fetch(url, {
headers: { 'Content-Type': 'multipart/form-data' }, // ❌ no boundary
body: form
});
// Don't use JSON.parse(JSON.stringify()) for deep clone
const clone = JSON.parse(JSON.stringify(obj)); // ❌ loses types
// Don't try to clone functions
structuredClone({ fn: () => {} }); // ❌ throws
// Don't use a detached buffer after transfer
structuredClone(obj, { transfer: [buf] });
console.log(buf.byteLength); // 0
// Don't assume FormData is JSON
// It is multipart/form-data, not application/json // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Content-Type missing boundary | Set manually | Let fetch set it |
structuredClone throws | Function or DOM node | Remove or convert |
| Detached buffer | Transferred to clone | Use the clone’s buffer |
File name missing | Used Blob instead | Construct File |
formData.get() returns string | Field is a string | Check instanceof File |
| Circular JSON error | Used JSON.stringify | Use structuredClone |
Date becomes string | Used JSON clone | Use structuredClone |
Real-World Examples
1. File Upload
const form = new FormData();
form.append('file', fileInput.files[0]);
await fetch('/upload', { method: 'POST', body: form });
2. Multiple Files
for (const file of files) {
form.append('files', file);
}
3. Blob from String
const blob = new Blob(['hello'], { type: 'text/plain' });
4. Blob to Buffer
const buffer = Buffer.from(await blob.arrayBuffer());
5. Download Blob
const response = await fetch(url);
const blob = await response.blob();
fs.writeFileSync('file.bin', Buffer.from(await blob.arrayBuffer()));
6. File with Metadata
const file = new File([data], 'report.pdf', {
type: 'application/pdf',
lastModified: Date.now()
});
7. Deep Clone for Storage
const snapshot = structuredClone(state);
8. Transfer to Worker
worker.postMessage(data, [data.buffer]);
9. Clone with Circular
const cloned = structuredClone(circularObj);
10. FormData from Object
const form = new FormData();
Object.entries(obj).forEach(([k, v]) => form.append(k, v));
Visual
FormData and fetch Integration
┌─────────────────────────────────────────────────────────────┐
│ FORMDATA → FETCH → SERVER │
│ │
│ const form = new FormData() │
│ form.append('name', 'Alice') │
│ form.append('file', file) │
│ │ │
│ ▼ │
│ fetch(url, { method: 'POST', body: form }) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Content-Type: multipart/form-data; boundary=... │ │
│ │ │ │
│ │ --boundary │ │
│ │ Content-Disposition: form-data; name="name" │ │
│ │ │ │
│ │ Alice │ │
│ │ --boundary │ │
│ │ Content-Disposition: form-data; name="file"; │ │
│ │ filename="file.txt" │ │
│ │ Content-Type: text/plain │ │
│ │ │ │
│ │ file contents │ │
│ │ --boundary-- │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Blob, File, Buffer Relationship
┌─────────────────────────────────────────────────────────────┐
│ BLOB │
│ ├── Immutable binary data │
│ ├── Optional MIME type │
│ ├── size, type properties │
│ ├── text(), arrayBuffer(), bytes(), stream() │
│ └── Transferable, clonable │
│ │
│ FILE (extends Blob) │
│ ├── Everything Blob has │
│ ├── name property │
│ ├── lastModified property │
│ └── Used in FormData for uploads │
│ │
│ BUFFER (Node.js) │
│ ├── Mutable binary data │
│ ├── Fast, zero-copy operations │
│ ├── Not clonable by structuredClone │
│ └── Convert: Buffer.from(await blob.arrayBuffer()) │
│ │
└─────────────────────────────────────────────────────────────┘
structuredClone Algorithm
┌─────────────────────────────────────────────────────────────┐
│ STRUCTURED CLONE ALGORITHM │
│ │
│ Input: { date, map, set, buffer, self } │
│ │ │
│ ▼ │
│ 1. Create empty clone │
│ 2. For each property: │
│ ├── Date → new Date(same time) │
│ ├── Map → new Map(cloned entries) │
│ ├── Set → new Set(cloned values) │
│ ├── ArrayBuffer → copy bytes │
│ ├── Circular ref → point to clone │
│ └── Function → throw DataCloneError │
│ 3. Return clone │
│ │
│ Preserves types that JSON cannot represent. │
│ │
└─────────────────────────────────────────────────────────────┘
Transfer vs Clone
┌─────────────────────────────────────────────────────────────┐
│ CLONE (default) │
│ │
│ original.buffer ────copy────▶ clone.buffer │
│ original.buffer still valid │
│ │
│ Use when: both need access │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ TRANSFER │
│ │
│ original.buffer ────move────▶ clone.buffer │
│ original.buffer detached (byteLength = 0) │
│ │
│ Use when: ownership moves to clone │
│ Saves memory copy │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
FormData | Multipart form construction |
FormData body | multipart/form-data |
Content-Type | Set automatically by fetch |
Blob | Immutable binary data |
Blob methods | text(), arrayBuffer(), bytes(), stream() |
File | Blob with name and lastModified |
structuredClone | Deep clone with structured clone algorithm |
| Circular refs | Preserved |
| Transfer | Detaches original |
| Clone error | DataCloneError |
| Native since | FormData v18, Blob v18, structuredClone v17 |
Key takeaways:
FormDataconstructs multipart bodies. Passing aFormDatatofetchas the body setsContent-Type: multipart/form-datawith a boundary automatically. Do not setContent-Typemanually; the boundary is required andfetchgenerates it .Blobis immutable binary data. It is constructed from strings, buffers, or typed arrays and cannot be modified. Read methods return promises. It is transferable and clonable, which makes it suitable forpostMessage,FormData, andstructuredClone.Fileis aBlobwith metadata. ThenameandlastModifiedproperties distinguish it. UseFilefor uploads that require a filename. UseBlobfor raw data without identity.structuredClonereplaces JSON deep-clone hacks. It preservesDate,Map,Set,RegExp,ArrayBuffer, and circular references. JSON serialization loses all of these. Functions and DOM nodes throwDataCloneError.- Transfer detaches the original. Passing an
ArrayBufferin thetransferoption moves it to the clone, leaving the original detached. This saves a memory copy when ownership moves. - The structured clone algorithm is consistent. It is the same algorithm used by
postMessageandIndexedDB. A value that survivesstructuredClonewill survive a worker transfer. A value that throws onstructuredClonewill throw onpostMessage. - These APIs are native and stable.
BlobandFormDataare stable since Node.js 18.structuredCloneis stable since Node.js 17. No imports are needed for the globals;Blobcan be imported fromnode:bufferfor explicitness.
Remember: FormData, Blob, File, and structuredClone are the Web Platform’s answer to multipart forms, binary data, file handling, and deep cloning. Node.js implements them natively, which means code written against them runs in both the browser and the server without modification. Use FormData for uploads. Use Blob for immutable binary data. Use File when a name matters. Use structuredClone when you need a deep copy that preserves types. They are designed as a family, and they work best when used together.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!