Switch to Rezo

From node-fetch

node-fetch brings the browser’s fetch() API to Node.js. It does one thing and does it well — but everything beyond a basic request requires manual work: parsing JSON, handling cookies, implementing retries, setting timeouts, managing proxies. Rezo gives you a complete HTTP client where all of that ships built in.

Basic GET

// node-fetch
import fetch from 'node-fetch';

const response = await fetch('https://api.example.com/users');
if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();

// Rezo -- same fetch-style call, automatic JSON, automatic error throwing
import rezo from 'rezo';

const { data } = await rezo('https://api.example.com/users');

Rezo is callable just like fetch() — you can pass a URL directly. It auto-parses JSON and auto-throws on error status codes. You can also use named methods like rezo.get(), rezo.post(), etc.

With node-fetch, you must check response.ok manually — fetch does not throw on 4xx or 5xx responses. Rezo throws automatically with structured error objects.

POST with JSON

// node-fetch
const response = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Ada Lovelace', email: 'ada@example.com' }),
});
const data = await response.json();

// Rezo -- JSON body and Content-Type handled automatically
const { data } = await rezo.postJson('https://api.example.com/users', {
  name: 'Ada Lovelace',
  email: 'ada@example.com',
});

Headers

// node-fetch
const response = await fetch('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer token123',
    'Accept': 'application/json',
  },
});

// Rezo -- same pattern, but headers are a full RezoHeaders instance
const { data } = await rezo.get('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer token123',
    'Accept': 'application/json',
  },
});

Error Handling

// node-fetch -- manual error checking, no structure
try {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }
  const data = await response.json();
} catch (error) {
  if (error.type === 'system') {
    console.log('Network error:', error.message);
  } else {
    console.log('HTTP error:', error.message);
  }
}

// Rezo -- structured errors with codes and flags
import rezo, { RezoError } from 'rezo';

try {
  const { data } = await rezo.get('https://api.example.com/data');
} catch (error) {
  if (rezo.isRezoError(error)) {
    console.log(error.code);          // "REZ_HTTP_ERROR"
    console.log(error.status);        // 503
    console.log(error.isHttpError);   // true (any non-2xx response)
    console.log(error.isTimeout);     // false
    console.log(error.isRetryable);   // true (HTTP errors are retryable)
    console.log(error.suggestion);    // "Check the status code and response body for more details..."
  }
}

Timeouts

// node-fetch -- manual AbortController, single timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch('https://api.example.com/data', {
    signal: controller.signal,
  });
} finally {
  clearTimeout(timeout);
}

// Rezo -- a single timeout covers the whole request
const { data } = await rezo.get('https://api.example.com/data', {
  timeout: 5_000
});

// Or staged via StagedTimeoutConfig:
const { data: staged } = await rezo.get('https://api.example.com/data', {
  timeout: {
    connect: 3_000,
    headers: 5_000,
    body: 15_000,
    total: 20_000,
  }
});

Streaming

// node-fetch
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

const response = await fetch('https://example.com/large-file.zip');
await pipeline(response.body, createWriteStream('./file.zip'));

// Rezo -- built-in downloads with progress events
const download = rezo.download('https://example.com/large-file.zip', './file.zip');

download.on('progress', ({ percentage, loaded, total }) => {
  console.log(`${percentage}% complete (${loaded}/${total} bytes)`);
});

download.on('finish', (info) => {
  console.log('Saved to', info.fileName);
});

File Download with Progress

node-fetch has no built-in progress tracking. You need to pipe the stream through a transform and calculate bytes manually:

// node-fetch -- manual progress tracking
import fetch from 'node-fetch';
import { createWriteStream } from 'node:fs';

const response = await fetch('https://example.com/archive.tar.gz');
const total = Number(response.headers.get('content-length'));
let transferred = 0;

response.body.on('data', (chunk) => {
  transferred += chunk.length;
  console.log(`${((transferred / total) * 100).toFixed(1)}%`);
});

const dest = createWriteStream('./archive.tar.gz');
response.body.pipe(dest);
await new Promise((resolve, reject) => {
  dest.on('finish', resolve);
  dest.on('error', reject);
});

// Rezo -- one method call
const dl = rezo.download('https://example.com/archive.tar.gz', './archive.tar.gz');
dl.on('progress', ({ percentage }) => console.log(`${percentage}%`));
await new Promise((resolve, reject) => {
  dl.once('finish', resolve);
  dl.once('error', reject);
});

Cookies

node-fetch does not manage cookies at all. You must parse Set-Cookie headers and attach cookies manually on every request:

// node-fetch -- manual cookie handling
let cookies = '';

const loginRes = await fetch('https://example.com/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ user: 'ada', pass: 'secret' }),
  redirect: 'manual',
});
cookies = loginRes.headers.raw()['set-cookie']?.join('; ') || '';

const dashRes = await fetch('https://example.com/dashboard', {
  headers: { Cookie: cookies },
});

// Rezo -- automatic cookie management
import rezo, { CookieJar } from 'rezo';

const jar = new CookieJar();
const client = rezo.create({ jar });

await client.postJson('https://example.com/login', { user: 'ada', pass: 'secret' });
const { data } = await client.get('https://example.com/dashboard');
// Cookies sent and received automatically

Retry Logic

node-fetch has no retry support. You must build your own retry loop with backoff:

// node-fetch -- DIY retry
async function fetchWithRetry(url, retries = 3, delay = 1000) {
  for (let i = 0; i < retries; i++) {
    try {
      const res = await fetch(url);
      if (res.ok) return res;
    } catch {}
    await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
  }
  throw new Error('Max retries exceeded');
}

// Rezo -- built-in retry with exponential backoff
const { data } = await rezo.get('https://api.example.com/data', {
  retry: {
    limit: 3,
    delay: 1000,
    maxDelay: 10_000,
    backoff: 'exponential',
    statusCodes: [429, 500, 502, 503, 504],
  }
});

Proxy Support

node-fetch requires a separate agent for proxy support:

// node-fetch -- requires https-proxy-agent
import fetch from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';

const agent = new HttpsProxyAgent('http://proxy:8080');
const response = await fetch('https://example.com', { agent });

// Rezo -- built-in proxy support
const { data } = await rezo.get('https://example.com', {
  proxy: 'http://proxy:8080',
});

What You Gain by Switching

FeatureRezonode-fetch
Automatic JSON parsingYesNo — manual .json()
Throws on HTTP errorsYesNo — must check .ok
Cookie jarBuilt-inNo
Retry with backoffBuilt-inNo
TimeoutsStaged (connect, headers, body, total)Manual AbortController
Progress eventsDownloads and uploadsNo
Proxy supportBuilt-in (HTTP, HTTPS, SOCKS4, SOCKS5)Separate agent package
Proxy rotationBuilt-in with health monitoringNo
HTTP/2Built-inNo
Stealth mode18 browser profilesNo
InterceptorsRequest and responseNo
Hooks26 lifecycle hooksNo
TypeScript typesFirst-classCommunity
Browser supportYes (Fetch and XHR adapters)No
Request queueBuilt-in with rate limitingNo
Response cachingBuilt-inNo
DNS cachingBuilt-inNo