v2.2.0 — stable

The HTTP client
that never throws.

Result<T,E> returns instead of try/catch hell. Retry, circuit breaker, dedup, OpenTelemetry, GraphQL, WebSocket, SSE — all built-in, zero extra packages.

$ npm install reixo
View on GitHub
api.ts
import { createClient } from 'reixo';

const api = createClient({ baseURL: 'https://api.example.com' });

// No try/catch. No .catch(). Never throws.
const result = await api.tryGet<User>('/users/42');

if (result.ok) {
  console.log(result.data.name);    // ✓ User — fully typed
} else {
  console.error(result.error.code); // ✓ NetworkError | HttpError
}

// Functional style
const name = result.map(u => u.name).unwrapOr('Anonymous');
0
dependencies
<25kb
gzipped
17+
built-in features
6
runtimes supported
The Problem

You're fighting your HTTP client.

Every team ends up writing the same boilerplate. The same fragile error handling. The same half-baked retry logic buried in utils.

✗
Before — every codebase has this
old-api.ts
// Repeated in every project...
async function fetchUser(id: string) {
  try {
    const res = await fetch(`/api/users/${id}`, {
      headers: { Authorization: `Bearer ${getToken()}` },
    });

    if (!res.ok) {
      // Different error shapes in every codebase
      const body = await res.json().catch(() => ({}));
      throw new Error(body.message ?? `HTTP ${res.status}`);
    }

    return res.json() as Promise<User>;
  } catch (err) {
    // Network error? Timeout? 4xx? 5xx?
    // No idea. Log and hope for the best.
    console.error('fetchUser failed', err);
    throw err;
  }
}
✓
After reixo — clean, typed, exhaustive
api.ts
// Typed. Never throws. Exhaustive.
const result = await api.tryGet<User>(`/users/${id}`);

if (result.ok) {
  return result.data; // User — fully typed ✓
}

// result.error is NetworkError | HttpError — typed too
if (result.error.type === 'HTTP_ERROR') {
  return handleStatus(result.error.status);
}

// TypeScript flags if you forget a branch
return fallback;
Features

Everything you need. Nothing you don't.

Every feature is built on the native Fetch API. No hidden transitive dependencies. Fully tree-shakeable.

Result<T, E> Returns

Every request returns Ok<T> | Err<E>. TypeScript enforces exhaustive handling at compile time. No more silent failures.

type-safe errors no try/catch .map() .unwrap()

Smart Retry + Backoff

Exponential backoff with jitter, per-request override, configurable status codes, and an onRetry hook. No external packages needed.

exponential backoff jitter per-request

Circuit Breaker

Opens after a failure threshold, half-opens to probe recovery. Protects your app from cascading failures without any extra libraries.

open/half-open configurable threshold

Request Deduplication

Identical in-flight GETs are coalesced into one network call. Ten components asking for the same data? One request. Zero config.

in-flight dedup SSR-safe React-ready

Zero-Dep OpenTelemetry

Automatic W3C traceparent propagation without @opentelemetry/api. Hooks for full OTLP span export.

traceparent W3C trace context zero deps

GraphQL · WebSocket · SSE

First-class GraphQL client. WebSocket manager with auto-reconnect. Server-Sent Events with for await async iterator.

GraphQL WebSocket SSE

Offline Queue

Requests made offline queue locally and auto-replay when connectivity returns. Built on the Network Information API with localStorage persistence. PWA-ready.

auto-replay PWA-ready

Response Caching

In-memory and localStorage adapters built in. Respects Cache-Control or set a custom TTL. Stale-while-revalidate pattern supported.

TTL caching stale-while-revalidate

Upload Progress + Pagination

Resumable chunked uploads with onProgress callbacks. Cursor, offset, and link-header pagination helpers built-in.

chunked upload pagination helpers
Code Examples

See it in action.

Real patterns for real production problems.

basic.ts
import { createClient } from 'reixo';

const api = createClient({
  baseURL: 'https://api.example.com',
  timeout: 10_000,
  headers: { 'X-API-Version': '2' },
});

// GET — typed, never throws
const result = await api.tryGet<User>('/users/1');
if (result.ok) console.log(result.data.name);

// POST
const post = await api.tryPost<Post>('/posts', {
  body: { title: 'Hello', content: '...' },
});

// Full REST
await api.tryPut('/users/1', { body: { role: 'admin' } });
await api.tryPatch('/users/1', { body: { active: true } });
await api.tryDelete('/users/1');
retry.ts
import { createClient } from 'reixo';

const api = createClient({
  baseURL: 'https://api.example.com',
  retry: {
    attempts: 3,
    strategy: 'exponential', // 'linear' | 'exponential' | 'fixed'
    baseDelay: 300,           // ms
    maxDelay: 5_000,
    jitter: true,             // prevents thundering herd
    retryOn: [408, 429, 503],
    onRetry: ({ attempt, error, delay }) => {
      console.log(`Retry ${attempt} in ${delay}ms`);
    },
  },
});

// Retries up to 3x before returning Err
const result = await api.tryGet<Data>('/flaky');

// Per-request override
await api.tryGet('/health', { retry: { attempts: 1 } });
circuit-breaker.ts
import { createClient, CircuitBreakerState } from 'reixo';

const api = createClient({
  baseURL: 'https://payments.example.com',
  circuitBreaker: {
    threshold: 5,          // open after 5 consecutive failures
    resetTimeout: 30_000,  // probe recovery after 30s
    onStateChange: (state: CircuitBreakerState) => {
      metrics.gauge('circuit_state', state); // CLOSED | OPEN | HALF_OPEN
    },
  },
});

const result = await api.tryPost<ChargeResult>('/charge', { body });

if (!result.ok) {
  if (result.error.type === 'CIRCUIT_OPEN') {
    return { status: 'degraded' }; // fast-fail gracefully
  }
}
graphql.ts
import { createGraphQLClient } from 'reixo';

const gql = createGraphQLClient({
  endpoint: 'https://api.example.com/graphql',
  headers: { Authorization: `Bearer ${token}` },
});

// Query
const result = await gql.query<{ user: User }>({
  query: `query GetUser($id: ID!) {
    user(id: $id) { id name email avatar }
  }`,
  variables: { id: '42' },
});

if (result.ok) console.log(result.data.user.name);

// Mutation
await gql.mutate<{ updateUser: User }>({
  mutation: `mutation Update($id: ID!, $name: String!) {
    updateUser(id: $id, name: $name) { id name }
  }`,
  variables: { id: '42', name: 'Alice' },
});
auth-interceptor.ts
import { createClient } from 'reixo';

const api = createClient({ baseURL: 'https://api.example.com' });

// Attach JWT on every request
api.interceptors.request.use(async (config) => {
  const token = await tokenStore.getAccessToken();
  config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Auto-refresh on 401
api.interceptors.response.use(
  (res) => res,
  async (error, original) => {
    if (error.status === 401 && !original._retry) {
      original._retry = true;
      await tokenStore.refresh();
      return api.request(original); // replay with new token
    }
    return Promise.reject(error);
  },
);
realtime.ts
import { createClient, createWebSocketManager } from 'reixo';

const api = createClient({ baseURL: 'https://api.example.com' });

// Server-Sent Events — async iterator
for await (const event of api.sse('/stream/events')) {
  console.log(event.type, event.data);
  if (event.type === 'done') break;
}

// WebSocket with auto-reconnect
const ws = createWebSocketManager('wss://ws.example.com/live', {
  reconnect: {
    maxAttempts: 10,
    delay: 1_000,
    backoff: 'exponential',
  },
  onMessage: (msg) => dispatch(parseMessage(msg)),
  onReconnect: (n) => console.log(`Reconnect attempt ${n}`),
  onClose: () => setStatus('disconnected'),
});

ws.send({ type: 'subscribe', channels: ['prices', 'orders'] });
Comparison

How does reixo stack up?

Every check below would have been a separate npm package. reixo ships it all.

Feature reixo axios ky ofetch got
Result<T,E> returns✓✗✗✗✗
Zero dependencies✓✗✓✓✗
Circuit breaker✓✗✗✗✗
Request deduplication✓✗✗✗✗
Built-in retry✓~✓✓✓
OpenTelemetry tracing✓✗✗✗✗
GraphQL support✓✗✗✗✗
WebSocket + SSE✓✗✗✗✗
Offline queue✓✗✗✗✗
Edge / Workers runtime✓✗✓✓✗
Mock adapter for tests✓✓✗✗✗

~ partial/plugin  ·  ✓ built-in  ·  ✗ unavailable

Platforms

Runs everywhere Fetch runs.

One API, six runtimes, zero adapter switching.

⬢
Node.js
v20+
🍞
Bun
v1+
🦕
Deno
v2+
🌐
Browser
all modern
☁️
CF Workers
Edge runtime
▲
Vercel Edge
Edge Functions
Get Started

Up and running in 60 seconds.

npm
npm install reixo
pnpm
pnpm add reixo
yarn
yarn add reixo
bun
bun add reixo
quickstart.ts
import { createClient } from 'reixo';

// Create once, export for reuse
const api = createClient({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 8_000,
  retry: { attempts: 2 },
});

interface Todo { id: number; title: string; completed: boolean }

// Never throws — result is Ok<Todo> | Err<NetworkError | HttpError>
const result = await api.tryGet<Todo>('/todos/1');

if (result.ok) {
  console.log('Title:', result.data.title);
  console.log('Done:', result.data.completed);
} else {
  // TypeScript knows exactly what error type this is
  console.error('Error:', result.error.message);
}

Stop writing
HTTP boilerplate.

reixo handles retries, error typing, circuit breaking, and tracing so you can ship features instead of infrastructure.