Published on

reixo 2.2.2 — The TypeScript HTTP Client That Handles Everything You Forgot to Handle

Authors

Introduction

Every production app eventually builds the same wrapper around fetch or axios: retry logic, timeout handling, error normalization, maybe a circuit breaker. You rebuild this in every project. reixo is that wrapper — done once, done right, with a TypeScript-first API that makes the happy path easy and the failure path explicit.

Installation

npm install reixo
# or
yarn add reixo

Result<T, E> — No More try/catch Everywhere

The first thing you notice about reixo is that it doesn't throw. Every request returns a Result<T, E> — a discriminated union that forces you to handle both success and failure at the call site.

import { reixo } from 'reixo'

const client = reixo.create({ baseURL: 'https://api.example.com' })

const result = await client.get<User>('/users/1')

if (result.ok) {
  console.log(result.data)  // User — fully typed
} else {
  console.error(result.error.message)  // NetworkError | HttpError | TimeoutError
}

No uncaught promise rejections. No forgetting to wrap things in try/catch. The type system makes it impossible to use result.data without checking result.ok first.

Compare this to the standard pattern:

// ❌ Standard fetch — easy to forget error handling
const res = await fetch('/users/1')
const user = await res.json()  // throws on network error
// Did you check res.ok? Did you catch the json() parse error?

// ✅ reixo — error handling is enforced by the type
const result = await client.get<User>('/users/1')
if (!result.ok) return  // handle it or explicitly skip
const user = result.data

Fluent HTTPBuilder

For complex requests, the fluent builder composes cleanly:

import { HTTPBuilder } from 'reixo'

const user = await new HTTPBuilder()
  .baseURL('https://api.example.com')
  .path('/users/search')
  .method('GET')
  .query({ role: 'admin', active: 'true' })
  .header('Authorization', `Bearer ${token}`)
  .timeout(5000)
  .retry(3)
  .send<User[]>()

if (user.ok) {
  console.log(`Found ${user.data.length} admin users`)
}

The builder is fully typed — .send<T>() returns Promise<Result<T, RequestError>>.

Retry with Exponential Backoff and Jitter

Retrying on network failures is table stakes. reixo's retry is configurable with exponential backoff and full jitter to avoid thundering herd:

const client = reixo.create({
  baseURL: 'https://api.example.com',
  retry: {
    attempts: 3,
    delay: 1000,           // base delay in ms
    backoff: 'exponential', // 1s, 2s, 4s
    jitter: true,           // randomize to spread retries
    retryOn: [429, 503, 504],  // only retry on these status codes
  },
})

// Automatically retries up to 3 times on 429/503/504
const result = await client.get('/data')

You can also retry on specific error types:

const client = reixo.create({
  retry: {
    attempts: 5,
    retryOnNetworkError: true,  // retry on connection reset, DNS failure, etc.
    shouldRetry: (error, attempt) => {
      // custom retry logic
      return attempt < 3 && error.status !== 401
    },
  },
})

Circuit Breaker

When a downstream service is degraded, hammering it with retries makes things worse. reixo's circuit breaker moves through three states automatically:

const client = reixo.create({
  baseURL: 'https://flaky-service.example.com',
  circuitBreaker: {
    threshold: 5,        // open after 5 consecutive failures
    timeout: 30_000,     // try again after 30 seconds (HALF_OPEN)
    onOpen: () => console.warn('Circuit opened — service appears down'),
    onClose: () => console.log('Circuit closed — service recovered'),
    onHalfOpen: () => console.log('Circuit half-open — testing...'),
  },
})

// State: CLOSED → normal operation
// After 5 failures: OPEN → requests fail immediately (no network call)
// After 30s: HALF_OPEN → one request let through to test
// If test succeeds: CLOSED again
// If test fails: OPEN again for another 30s

The circuit breaker is per-client, so you can have separate breakers for different downstream services with different thresholds.

Request Deduplication

When the same request fires multiple times concurrently (race condition on page load, double-click, etc.), reixo can collapse them into a single network request:

const client = reixo.create({
  deduplication: true,  // identical in-flight requests share a single fetch
})

// These fire simultaneously:
const [r1, r2, r3] = await Promise.all([
  client.get('/users/profile'),
  client.get('/users/profile'),
  client.get('/users/profile'),
])

// Only ONE network request made — all three resolve with the same data

Deduplication is keyed on method + URL + query params. POST requests are never deduplicated (they have side effects).

LRU Caching with Multiple Strategies

const client = reixo.create({
  cache: {
    strategy: 'stale-while-revalidate',  // or 'cache-first' | 'network-first'
    maxAge: 60_000,   // cache for 60 seconds
    maxSize: 100,     // LRU: evict least recently used after 100 entries
  },
})

// cache-first: return cache immediately, use network only on miss
// network-first: always fetch, cache the result
// stale-while-revalidate: return stale cache immediately, refetch in background
const result = await client.get('/config')

Cache entries are keyed by URL + query string. You can also bypass or invalidate the cache manually:

// Bypass cache for this request
const fresh = await client.get('/users', { cache: 'no-store' })

// Invalidate a specific cache entry
client.cache.delete('/config')

// Clear all cached entries
client.cache.clear()

GraphQL Support

reixo has first-class GraphQL support — no separate library needed:

const result = await client.graphql<{ user: User }>({
  query: `
    query GetUser($id: ID!) {
      user(id: $id) {
        id
        name
        email
        role
      }
    }
  `,
  variables: { id: '123' },
})

if (result.ok) {
  console.log(result.data.user.name)
}

GraphQL errors (from the errors field in the response) are surfaced in result.error just like HTTP errors — consistent Result<T,E> API regardless of transport.

WebSocket and Server-Sent Events

// WebSocket — typed messages
const ws = client.websocket<ChatMessage>('/ws/chat')

ws.on('message', (msg) => {
  console.log(`${msg.author}: ${msg.text}`)
})

ws.on('error', (err) => {
  console.error('WebSocket error:', err)
})

ws.send({ type: 'join', room: 'general' })

// SSE — real-time server push
const sse = client.sse<StockPrice>('/stream/prices')

for await (const event of sse) {
  if (event.ok) {
    console.log(`${event.data.ticker}: $${event.data.price}`)
  }
}

The SSE API is an async iterator — you loop through events naturally with for await. Reconnection is handled automatically with configurable delay.

OpenTelemetry Tracing

reixo automatically propagates W3C traceparent headers when you pass in a tracer:

import { trace } from '@opentelemetry/api'
import { reixo } from 'reixo'

const client = reixo.create({
  baseURL: 'https://api.example.com',
  telemetry: {
    tracer: trace.getTracer('my-service'),
    propagateHeaders: true,  // adds traceparent to all outgoing requests
  },
})

// Requests are automatically traced as child spans
// Headers include: traceparent: 00-{traceId}-{spanId}-01
const result = await client.get('/orders')

Every request creates a span with attributes for HTTP method, URL, status code, and duration. Failed requests set the span status to ERROR automatically.

MockAdapter for Testing

import { MockAdapter } from 'reixo'

const mock = new MockAdapter()

mock.onGet('/users/1').reply(200, {
  id: '1',
  name: 'Sanjeev Sharma',
  email: 'sanjeev@example.com',
})

mock.onPost('/users').reply(201, { id: '2', name: 'New User' })

mock.onGet('/broken').reply(500, { message: 'Internal Server Error' })

const client = reixo.create({ adapter: mock })

// In tests — no real network calls
const result = await client.get<User>('/users/1')
expect(result.ok).toBe(true)
expect(result.data.name).toBe('Sanjeev Sharma')

MockAdapter also supports delays (to test timeout behavior), network errors, and call tracking:

// Verify the request was made
expect(mock.history.get[0].url).toBe('/users/1')

// Test timeout behavior
mock.onGet('/slow').replyWithDelay(5000, 200, {})

ResumableUploader

For large file uploads that might fail mid-way:

import { ResumableUploader } from 'reixo'

const uploader = new ResumableUploader({
  client,
  url: '/upload/video',
  file: videoFile,        // File | Blob | Buffer
  chunkSize: 5 * 1024 * 1024,  // 5MB chunks
  onProgress: (pct) => console.log(`${pct}% uploaded`),
})

const result = await uploader.start()

// If connection drops mid-upload:
// uploader.pause()   → stop sending chunks
// uploader.resume()  → continue from last successful chunk (server-side resume)

The server must implement a resumable upload protocol (e.g. tus.io). reixo handles the chunk coordination, retries on failed chunks, and progress tracking.

BatchProcessor

Fire multiple requests and process results as they complete:

import { BatchProcessor } from 'reixo'

const batch = new BatchProcessor(client, { concurrency: 5 })

const userIds = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']

const results = await batch.process(
  userIds.map((id) => ({ method: 'GET', path: `/users/${id}` }))
)

results.forEach((result, i) => {
  if (result.ok) {
    console.log(`User ${userIds[i]}: ${result.data.name}`)
  }
})

concurrency: 5 means at most 5 requests are in-flight at any time — avoids overwhelming the server while still being faster than sequential requests.

Why reixo Over axios or fetch?

Featurefetchaxiosreixo
TypeScript-firstpartialpartial✅ native
Result<T,E> API
Retry with jittermanualplugin✅ built-in
Circuit breakermanualmanual✅ built-in
Request deduplicationmanualmanual✅ built-in
LRU cachemanualmanual✅ built-in
GraphQLmanualmanual✅ built-in
WebSocket/SSEseparateseparate✅ built-in
OTel tracingmanualplugin✅ built-in
Mock adaptermanualaxios-mock-adapter✅ built-in
Resumable uploadsmanualmanual✅ built-in

Conclusion

reixo v2.2.2 bundles everything a production HTTP layer needs: explicit error handling with Result<T,E>, retry with jitter, circuit breaking, request deduplication, multi-strategy caching, GraphQL, WebSocket/SSE, OpenTelemetry, and a MockAdapter for tests. If you're tired of rebuilding the same HTTP infrastructure in every project, give reixo a try — npm install reixo and you get all of it, typed from top to bottom.