Utilities

RezoFormData

RezoFormData is a universal FormData wrapper built on the native FormData API. It works across Node.js 18+, Bun, Deno, browsers, Cloudflare Workers, and edge runtimes. It adds Buffer conversion, synchronous content-type access, object serialization with nested key support, and URL-encoded form creation.

import { RezoFormData } from 'rezo';

Basic Usage

const form = new RezoFormData();

// Append text fields
form.append('username', 'johndoe');
form.append('email', 'john@example.com');

// Append a file
const file = new Blob(['file content'], { type: 'text/plain' });
form.append('avatar', file, 'avatar.png');

// Use with Rezo
import rezo from 'rezo';
const response = await rezo.post('https://api.example.com/upload', form);

Field Operations

append(name, value, filename?)

Add a field. Accepts string, Blob, or Buffer. Buffer values are automatically converted to Blob:

form.append('name', 'Alice');
form.append('photo', new Blob([imageData], { type: 'image/png' }), 'photo.png');
form.append('document', Buffer.from('PDF content'), 'report.pdf');

set(name, value, filename?)

Replace an existing field (or add if it does not exist):

form.set('name', 'Bob');  // Replaces previous 'name' value

get(name) / getAll(name)

Retrieve field values:

form.get('name');     // 'Bob'
form.getAll('tags');  // ['js', 'typescript']

has(name) / delete(name)

form.has('name');     // true
form.delete('name');
form.has('name');     // false

Iteration

for (const [key, value] of form) {
  console.log(key, value);
}

form.forEach((value, key) => {
  console.log(key, value);
});

Synchronous Content-Type

getContentType() returns the multipart/form-data content type with boundary synchronously. The boundary is eagerly generated at construction time:

const form = new RezoFormData();
form.append('field', 'value');

form.getContentType();
// 'multipart/form-data; boundary=----RezoFormBoundarya1b2c3d4e5f6...'

This is critical for Rezo’s request pipeline, which needs the content type before the async serialization step. After the first async operation (toBuffer, toArrayBuffer, etc.), the cached content type reflects the actual boundary used by the native FormData serialization.

Async Content-Type

For the exact content type with the native boundary after serialization:

const contentType = await form.getContentTypeAsync();

Buffer Conversion

toBuffer() (Node.js/Bun/Deno)

const buffer = await form.toBuffer();
// Returns Node.js Buffer

toArrayBuffer() (Universal)

const ab = await form.toArrayBuffer();
// Returns ArrayBuffer (works everywhere)

toUint8Array() (Universal)

const bytes = await form.toUint8Array();
// Returns Uint8Array

Synchronous Buffer Access

After any async conversion, the result is cached. Use getBuffer() and getLengthSync() for synchronous access:

await form.toBuffer();  // Triggers caching

form.getBuffer();       // Returns cached Buffer (null if not cached)
form.getLengthSync();   // Returns cached byte length (undefined if not cached)

Async Length and Headers

const length = await form.getLength();
// Returns byte length as number

const headers = await form.getHeadersAsync();
// { 'content-type': 'multipart/form-data; boundary=...', 'content-length': '1234' }

fromObject() — Object Serialization

Create a RezoFormData from a plain object. Handles primitives, arrays, nested objects, Blobs, Buffers, Uint8Arrays, ArrayBuffers, and file descriptors.

Default Mode (JSON-encoded nested values)

By default, nested objects and arrays are JSON-stringified:

const form = RezoFormData.fromObject({
  name: 'Alice',
  age: 30,
  active: true,
  tags: ['admin', 'user'],
  address: { city: 'NYC', zip: '10001' },
});

// name=Alice, age=30, active=true
// tags=["admin","user"]
// address={"city":"NYC","zip":"10001"}

Nested Key Mode (Bracket notation)

Pass nestedKeys: true to flatten with bracket notation (compatible with PHP, Rails, Express/qs):

const form = RezoFormData.fromObject({
  user: {
    name: 'Alice',
    roles: ['admin', 'editor'],
    address: { city: 'NYC' },
  },
}, { nestedKeys: true });

// user[name]=Alice
// user[roles][0]=admin
// user[roles][1]=editor
// user[address][city]=NYC

File Descriptors

Objects with value and filename/contentType properties are treated as file uploads:

const form = RezoFormData.fromObject({
  document: {
    value: Buffer.from('PDF content'),
    filename: 'report.pdf',
    contentType: 'application/pdf',
  },
  photo: {
    value: new Blob([imageData]),
    filename: 'photo.jpg',
  },
});

Null/Undefined Handling

null and undefined values are silently skipped:

const form = RezoFormData.fromObject({
  name: 'Alice',
  nickname: null,       // Skipped
  bio: undefined,       // Skipped
});
// Only 'name' field is included

createUrlEncoded() — URL-Encoded Forms

Create a URL-encoded string for application/x-www-form-urlencoded POST bodies:

const body = RezoFormData.createUrlEncoded({
  username: 'alice',
  password: 's3cret',
  remember: true,
});
// 'username=alice&password=s3cret&remember=true'

await rezo.post('https://example.com/login', body, {
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
});

fromNativeFormData() — Convert Native FormData

Wrap an existing native FormData instance:

const native = new FormData();
native.append('field', 'value');
native.append('file', someBlob, 'doc.pdf');

const form = RezoFormData.fromNativeFormData(native);
// Now has all the RezoFormData methods

toNativeFormData()

Access the underlying native FormData when you need it for other APIs:

const native = form.toNativeFormData();
// Standard FormData object

Query String Conversion

Convert string-only fields to URL query string or URLSearchParams:

form.append('q', 'search term');
form.append('page', '1');

form.toUrlQueryString();
// 'q=search+term&page=1'

form.toURLSearchParams();
// URLSearchParams { 'q' => 'search term', 'page' => '1' }

Binary fields (Blob/File) are silently omitted from query string conversion.

Complete Example

import rezo, { RezoFormData } from 'rezo';

// Build a multipart form with file upload
const form = RezoFormData.fromObject({
  title: 'Quarterly Report',
  tags: ['finance', 'q4'],
  attachment: {
    value: Buffer.from(pdfBytes),
    filename: 'report.pdf',
    contentType: 'application/pdf',
  },
});

const response = await rezo.post('https://api.example.com/documents', form, {
  timeout: 30000,
});

console.log(response.data);