From Fetch API
The Fetch API is available in every modern runtime — browsers, Node.js 18+, Deno, Bun, and edge workers. It is minimal by design: no cookies, no retry, no automatic error throwing, no timeouts without AbortController, and no JSON shorthand. Rezo uses a Fetch adapter internally for browser and edge environments, but layers a complete feature set on top.
Basic GET
// Fetch API
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// Rezo -- drop-in replacement, automatic JSON, automatic error throwing
import rezo from 'rezo';
const { data } = await rezo('https://api.example.com/users'); Rezo is callable just like fetch() — same one-argument pattern. The difference: Rezo auto-parses JSON, auto-throws on error status codes, and returns { data, status, headers, cookies } instead of a raw Response. You can also use named methods:
const { data } = await rezo.get('https://api.example.com/users'); Fetch never throws on HTTP error status codes. You must check response.ok every time. Rezo throws a structured RezoError automatically.
POST with JSON
// Fetch API
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Ada Lovelace' }),
});
const data = await response.json();
// Rezo -- no manual serialization or Content-Type
const { data } = await rezo.postJson('https://api.example.com/users', {
name: 'Ada Lovelace',
}); Headers
// Fetch API
const response = await fetch('https://api.example.com/data', {
headers: new Headers({
'Authorization': 'Bearer token123',
'Accept': 'application/json',
}),
});
// Rezo -- plain objects or RezoHeaders
const { data } = await rezo.get('https://api.example.com/data', {
headers: {
'Authorization': 'Bearer token123',
'Accept': 'application/json',
},
}); Error Handling
Fetch’s lack of automatic error throwing is its most common pain point:
// Fetch API -- no error on 4xx/5xx
const response = await fetch('https://api.example.com/missing');
console.log(response.ok); // false
console.log(response.status); // 404
// No error thrown -- you must check manually
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
// error.message is a plain string -- no structure
console.log(error.message); // "HTTP 404: Not Found"
}
// Rezo -- automatic errors with full structure
import rezo, { RezoError } from 'rezo';
try {
await rezo.get('https://api.example.com/missing');
} catch (error) {
if (rezo.isRezoError(error)) {
console.log(error.code); // "REZ_HTTP_ERROR"
console.log(error.status); // 404
console.log(error.isClientError); // true
console.log(error.isServerError); // false
console.log(error.isNetworkError); // false
console.log(error.suggestion); // "Check that the URL exists..."
}
} Timeouts
// Fetch API -- manual AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://api.example.com/slow', {
signal: controller.signal,
});
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request timed out');
}
} finally {
clearTimeout(timeoutId);
}
// Rezo -- staged timeouts, no boilerplate
const { data } = await rezo.get('https://api.example.com/slow', {
timeout: {
connect: 3_000,
headers: 5_000,
body: 15_000,
total: 20_000,
}
}); Streaming
// Fetch API -- manual stream consumption
const response = await fetch('https://example.com/stream');
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(value);
}
// Rezo -- streaming with response type
const response = await rezo.get('https://example.com/stream', {
responseType: 'stream',
});
response.data.on('data', (chunk) => process.stdout.write(chunk));
response.data.on('end', () => console.log('Done')); Cookies
The Fetch API in Node.js does not manage cookies. Browser fetch sends cookies for same-origin requests only, and provides no way to inspect or persist them:
// Fetch API -- no cookie management
// Must manually extract and resend cookies
const loginRes = await fetch('https://example.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user: 'ada', pass: 'secret' }),
});
const setCookie = loginRes.headers.get('set-cookie'); // raw string
// Parsing, storing, and resending is entirely on you
// 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 captured and sent automatically Retry
Fetch has no retry support. Any retry logic must be hand-written:
// Fetch API -- manual retry loop
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i <= retries; i++) {
try {
const response = await fetch(url);
if (response.ok) return await response.json();
if (i === retries) throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === retries) throw error;
}
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
}
// Rezo -- built-in retry
const { data } = await rezo.get('https://api.example.com/data', {
retry: {
limit: 3,
backoff: { delay: 1000, maxDelay: 10_000 },
statusCodes: [429, 500, 502, 503, 504],
}
}); Proxy
// Fetch API -- no proxy support in browsers, requires undici dispatcher in Node.js
// Node.js with undici
import { ProxyAgent } from 'undici';
const response = await fetch('https://example.com', {
dispatcher: new ProxyAgent('http://proxy:8080'),
});
// Rezo -- built-in proxy support everywhere
const { data } = await rezo.get('https://example.com', {
proxy: 'http://proxy:8080',
}); Query Parameters
// Fetch API -- manual URL construction
const params = new URLSearchParams({ page: '2', limit: '25' });
const response = await fetch(`https://api.example.com/users?${params}`);
// Rezo -- params as an object
const { data } = await rezo.get('https://api.example.com/users', {
params: { page: 2, limit: 25 },
}); What You Gain by Switching
| Feature | Rezo | Fetch API |
|---|---|---|
| Automatic JSON parsing | Yes | No — manual .json() |
| Throws on HTTP errors | Yes | No — must check .ok |
| Typed responses | rezo.get<T>() | No |
| Cookie jar | Built-in | No |
| Cookie persistence | JSON and Netscape | No |
| Retry with backoff | Built-in | No |
| Timeouts | Staged (connect, headers, body, total) | Manual AbortController |
| Interceptors | Request and response | No |
| Hooks | 26 lifecycle hooks | No |
| Progress events | Downloads and uploads | No |
| Proxy support | Built-in (HTTP, HTTPS, SOCKS4, SOCKS5) | No (browser) / Dispatcher (Node) |
| Proxy rotation | Built-in | No |
| HTTP/2 | Built-in | No |
| Stealth mode | 18 browser profiles | No |
| Request queue | Built-in with rate limiting | No |
| Response caching | Built-in | No |
| DNS caching | Built-in | No |
| Web crawler | Built-in | No |
| Site cloning | Built-in | No |
| Error codes | 70+ with recovery suggestions | None |
| TypeScript | Strict types, generics, overloads | Basic |
Interceptors
Fetch has no interceptor pattern. With Rezo, you can transform every request and response:
import rezo from 'rezo';
const client = rezo.create();
client.interceptors.request.use((config) => {
config.headers.set('Authorization', `Bearer ${getToken()}`);
return config;
});
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.status === 401) {
return refreshAndRetry(error.config);
}
return Promise.reject(error);
}
); Typed Responses
// Fetch API -- no generics
const response = await fetch('/api/users');
const data: unknown = await response.json();
// Rezo -- typed data
interface User {
id: number;
name: string;
}
const { data } = await rezo.get<User[]>('/api/users');
// data is User[]