Switch to Rezo

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

FeatureRezoFetch API
Automatic JSON parsingYesNo — manual .json()
Throws on HTTP errorsYesNo — must check .ok
Typed responsesrezo.get<T>()No
Cookie jarBuilt-inNo
Cookie persistenceJSON and NetscapeNo
Retry with backoffBuilt-inNo
TimeoutsStaged (connect, headers, body, total)Manual AbortController
InterceptorsRequest and responseNo
Hooks26 lifecycle hooksNo
Progress eventsDownloads and uploadsNo
Proxy supportBuilt-in (HTTP, HTTPS, SOCKS4, SOCKS5)No (browser) / Dispatcher (Node)
Proxy rotationBuilt-inNo
HTTP/2Built-inNo
Stealth mode18 browser profilesNo
Request queueBuilt-in with rate limitingNo
Response cachingBuilt-inNo
DNS cachingBuilt-inNo
Web crawlerBuilt-inNo
Site cloningBuilt-inNo
Error codes70+ with recovery suggestionsNone
TypeScriptStrict types, generics, overloadsBasic

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[]