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
| Feature | Rezo | node-fetch |
|---|---|---|
| Automatic JSON parsing | Yes | No — manual .json() |
| Throws on HTTP errors | Yes | No — must check .ok |
| Cookie jar | Built-in | No |
| Retry with backoff | Built-in | No |
| Timeouts | Staged (connect, headers, body, total) | Manual AbortController |
| Progress events | Downloads and uploads | No |
| Proxy support | Built-in (HTTP, HTTPS, SOCKS4, SOCKS5) | Separate agent package |
| Proxy rotation | Built-in with health monitoring | No |
| HTTP/2 | Built-in | No |
| Stealth mode | 18 browser profiles | No |
| Interceptors | Request and response | No |
| Hooks | 26 lifecycle hooks | No |
| TypeScript types | First-class | Community |
| Browser support | Yes (Fetch and XHR adapters) | No |
| Request queue | Built-in with rate limiting | No |
| Response caching | Built-in | No |
| DNS caching | Built-in | No |