reixo 2.2.2 — The TypeScript HTTP Client Built for Production Node.js 2026

Sanjeev SharmaSanjeev Sharma
8 min read

Advertisement

Introduction

Why This Matters

Every production application builds the same HTTP infrastructure: retry logic with backoff, timeout handling, normalized error types, circuit breakers for flaky dependencies, maybe caching. This is pure boilerplate — identical across projects, tedious to write correctly, and rarely tested.

reixo v2.2.2 is that infrastructure, built once and shipped as a single TypeScript-first package. Its defining feature is the Result<T, E> API: every request returns a discriminated union that the type system forces you to handle. You cannot forget error handling — the TypeScript compiler will not let you access result.data without first checking result.ok.

Installation

npm install reixo
# or
yarn add reixo

Result<T, E> — No More Uncaught Promise Rejections

The standard fetch API throws on network errors and does not throw on non-2xx responses. axios throws on non-2xx but requires isAxiosError checks. reixo returns Result<T, E> for all cases:

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.name)     // User — fully typed, no assertion needed
} else {
  console.error(result.error.message)  // NetworkError | HttpError | TimeoutError
}

Compare this to the defensive code fetch requires:

// fetch — three separate failure modes, easy to miss any one
try {
  const res = await fetch('/users/1')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)  // easy to forget
  const user = await res.json()                        // can also throw
  return user
} catch (err) {
  // what kind of error is this? no idea from the type
}
 
// reixo — one Result, all failure modes unified
const result = await client.get<User>('/users/1')
if (!result.ok) return  // TypeScript enforces this check
return result.data       // result.data is only accessible after checking result.ok

Fluent HTTPBuilder API

For complex requests, the fluent builder chains configuration readably:

import { HTTPBuilder } from 'reixo'
 
const result = await new HTTPBuilder()
  .baseURL('https://api.example.com')
  .path('/users/search')
  .method('GET')
  .query({ role: 'admin', active: 'true', page: '1' })
  .header('Authorization', `Bearer ${token}`)
  .timeout(5_000)
  .retry(3)
  .send<User[]>()
 
if (result.ok) {
  console.log(`Found ${result.data.length} admin users`)
}

.send<T>() returns Promise<Result<T, RequestError>> — fully typed from request to response.

Retry with Exponential Backoff and Jitter

Retrying without jitter causes thundering herd — all failed clients retry at the same time, spiking the server again. reixo adds randomization by default:

const client = reixo.create({
  baseURL: 'https://api.example.com',
  retry: {
    attempts: 3,
    delay: 1_000,            // base delay in ms
    backoff: 'exponential',  // 1s, 2s, 4s (before jitter)
    jitter: true,            // randomize within the window
    retryOn: [429, 503, 504],
    shouldRetry: (error, attempt) => {
      // Custom logic — never retry 401/403
      return attempt &lt;= 3 && error.status !== 401 && error.status !== 403
    },
  },
})

Circuit Breaker

Three states — CLOSED (normal), OPEN (failing fast), HALF_OPEN (testing recovery):

const paymentClient = reixo.create({
  baseURL: 'https://payments.example.com',
  circuitBreaker: {
    threshold: 5,      // open after 5 consecutive failures
    timeout: 30_000,   // wait 30s before entering HALF_OPEN
    onOpen: () => {
      console.warn('Payment service circuit opened — returning fallback')
      alerting.notify('payment_circuit_open')
    },
    onClose: () => console.log('Payment service recovered'),
    onHalfOpen: () => console.log('Testing payment service recovery...'),
  },
})
 
// When OPEN, requests fail immediately — no network call
const result = await paymentClient.get('/status')
if (!result.ok && result.error.type === 'CircuitOpen') {
  return { status: 'degraded', message: 'Payment service temporarily unavailable' }
}

The circuit breaker is per-client instance — create separate clients for each downstream service so one failing dependency does not affect others.

Request Deduplication

Collapses identical concurrent GET requests into a single network call:

const client = reixo.create({
  baseURL: 'https://api.example.com',
  deduplication: true,
})
 
// Three components mount simultaneously and all request the same config
const [r1, r2, r3] = await Promise.all([
  client.get('/app/config'),
  client.get('/app/config'),
  client.get('/app/config'),
])
// Only ONE HTTP request is made — all three resolve with identical data

Deduplication keys on method + URL + query string. POST, PUT, and DELETE requests are never deduplicated because they have side effects.

LRU Caching with Multiple Strategies

const client = reixo.create({
  cache: {
    strategy: 'stale-while-revalidate',  // 'cache-first' | 'network-first' | 'stale-while-revalidate'
    maxAge: 60_000,    // entries live 60 seconds
    maxSize: 100,      // LRU: evict least recently used after 100 entries
  },
})
 
// First call — network fetch, result cached
const r1 = await client.get('/config')
 
// Second call (within 60s) — served from cache immediately
// If stale-while-revalidate: returns stale data AND revalidates in background
const r2 = await client.get('/config')
 
// Bypass cache for a single request
const fresh = await client.get('/config', { cache: 'no-store' })
 
// Invalidate specific entries
client.cache.delete('/config')
client.cache.clear()

GraphQL Support

First-class GraphQL with the same Result API:

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)  // fully typed
}
// GraphQL errors (the errors[] field) surface in result.error just like HTTP errors

WebSocket and SSE

// WebSocket — typed messages, auto-reconnect
const ws = client.websocket<ChatMessage>('/ws/chat')
 
ws.on('message', (msg: ChatMessage) => {
  console.log(`${msg.author}: ${msg.text}`)
})
 
ws.send({ type: 'join', room: 'general' })
 
// Server-Sent Events — async iterator interface
const sse = client.sse<StockPrice>('/stream/prices')
 
for await (const event of sse) {
  if (event.ok) {
    console.log(`${event.data.ticker}: $${event.data.price}`)
  }
}
// Reconnection with configurable delay is handled automatically

OpenTelemetry Tracing

Propagates W3C traceparent headers and creates child spans for every request:

import { trace } from '@opentelemetry/api'
 
const client = reixo.create({
  baseURL: 'https://api.example.com',
  telemetry: {
    tracer: trace.getTracer('order-service'),
    propagateHeaders: true,  // adds traceparent to all outgoing requests
  },
})
 
// Every request creates a span: HTTP method, URL, status, duration
// Failed requests set span status to ERROR automatically
const result = await client.get('/orders')

MockAdapter for Testing

import { MockAdapter } from 'reixo'
 
const mock = new MockAdapter()
 
mock.onGet('/users/1').reply(200, { id: '1', name: 'Sanjeev Sharma' })
mock.onPost('/users').reply(201, { id: '2', name: 'New User' })
mock.onGet('/broken').reply(500, { message: 'Server Error' })
mock.onGet('/slow').replyWithDelay(5_000, 200, {})  // Test timeouts
 
const client = reixo.create({ adapter: mock })
 
// 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')
 
// Verify calls were made
expect(mock.history.get[0].url).toBe('/users/1')

Feature Comparison

Featurefetchaxiosreixo 2.2.2
TypeScript-firstpartialpartialnative
Result<T,E> APInonoyes
Retry with jittermanualpluginbuilt-in
Circuit breakermanualmanualbuilt-in
Request deduplicationmanualmanualbuilt-in
LRU cachemanualmanualbuilt-in
GraphQLmanualmanualbuilt-in
WebSocket/SSEseparateseparatebuilt-in
OTel tracingmanualpluginbuilt-in
Mock adaptermanualaxios-mock-adapterbuilt-in

Common Mistakes

Creating a new client per request. The circuit breaker and deduplication state are per-instance — create one client per downstream service at module level and reuse it.

Ignoring result.ok check. TypeScript will not prevent you from accessing result.data if you use type assertions. Always check result.ok — do not cast around it.

Setting retry on non-idempotent endpoints. Retrying a POST that creates a record can cause duplicates. Only configure retry on GET, PUT, and DELETE endpoints or explicitly idempotent POSTs.

Best Practices

  • Instantiate one reixo.create() client per downstream service at the module level — this ensures circuit breaker state is shared across all requests to that service.
  • Use tryGet / tryPost for all production code rather than the throwing API — this makes error handling explicit and eliminates uncaught Promise rejections.
  • Combine deduplication: true and cache: { strategy: 'stale-while-revalidate' } for read-heavy public endpoints that many components query on mount.
  • Pass an OTel tracer to the telemetry config to automatically propagate distributed traces through your service mesh without manual header injection.
  • Use MockAdapter in unit tests with replyWithDelay to test timeout handling and reply(503) to test circuit breaker state transitions.

Key Takeaways

  • reixo v2.2.2 returns Result<T, E> from every request — a discriminated union that makes error handling exhaustive at the type level without try/catch.
  • The fluent HTTPBuilder API chains method, URL, query, headers, timeout, and retry configuration for complex requests in a readable way.
  • Retry uses exponential backoff with jitter to prevent thundering herd when recovering from server failures — configurable per status code.
  • The circuit breaker moves through CLOSED, OPEN, and HALF_OPEN states automatically, stopping requests to degraded services without any manual intervention.
  • Request deduplication collapses identical concurrent GET requests into one network call — essential for components that mount simultaneously and fetch the same resource.
  • LRU caching supports cache-first, network-first, and stale-while-revalidate strategies with configurable TTL and maximum entry count.
  • GraphQL, WebSocket, and SSE share the same Result<T, E> interface as REST requests — consistent error handling regardless of transport.
  • MockAdapter enables unit testing with no network calls, including delay simulation for timeout tests and call history assertions.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading