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.