| |

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

MethodPurpose
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

FeatureBlobFile
DataImmutable binaryImmutable binary
MIME typetypetype
NameNoname
Last modifiedNolastModified
TransferableYesYes
ClonableYesYes
Read methodstext(), arrayBuffer(), bytes(), stream()Inherited

structuredClone

AspectBehavior
AlgorithmStructured clone
Circular refsPreserved
Date/Map/SetPreserved
ArrayBufferCloned or transferred
FunctionsThrows DataCloneError
DOM nodesThrows DataCloneError
Transfer optionDetaches originals

Cloneable Types

TypestructuredCloneJSON
DatePreservedString
MapPreserved{}
SetPreserved{}
RegExpPreserved{}
Uint8ArrayPreserved{}
Circular refsPreservedThrows
FunctionsThrowsOmitted

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

PitfallWhy It HappensFix
Content-Type missing boundarySet manuallyLet fetch set it
structuredClone throwsFunction or DOM nodeRemove or convert
Detached bufferTransferred to cloneUse the clone’s buffer
File name missingUsed Blob insteadConstruct File
formData.get() returns stringField is a stringCheck instanceof File
Circular JSON errorUsed JSON.stringifyUse structuredClone
Date becomes stringUsed JSON cloneUse 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

ItemValue
FormDataMultipart form construction
FormData bodymultipart/form-data
Content-TypeSet automatically by fetch
BlobImmutable binary data
Blob methodstext(), arrayBuffer(), bytes(), stream()
FileBlob with name and lastModified
structuredCloneDeep clone with structured clone algorithm
Circular refsPreserved
TransferDetaches original
Clone errorDataCloneError
Native sinceFormData v18, Blob v18, structuredClone v17

Key takeaways:

  • FormData constructs multipart bodies. Passing a FormData to fetch as the body sets Content-Type: multipart/form-data with a boundary automatically. Do not set Content-Type manually; the boundary is required and fetch generates it .
  • Blob is 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 for postMessage, FormData, and structuredClone .
  • File is a Blob with metadata. The name and lastModified properties distinguish it. Use File for uploads that require a filename. Use Blob for raw data without identity.
  • structuredClone replaces JSON deep-clone hacks. It preserves Date, Map, Set, RegExp, ArrayBuffer, and circular references. JSON serialization loses all of these. Functions and DOM nodes throw DataCloneError .
  • Transfer detaches the original. Passing an ArrayBuffer in the transfer option 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 postMessage and IndexedDB. A value that survives structuredClone will survive a worker transfer. A value that throws on structuredClone will throw on postMessage.
  • These APIs are native and stable. Blob and FormData are stable since Node.js 18. structuredClone is stable since Node.js 17. No imports are needed for the globals; Blob can be imported from node:buffer for 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!