Introducing reixo — The TypeScript HTTP Client That Replaces axios in 2026
Advertisement
Introduction
Why This Matters
Every production Node.js application eventually builds the same wrapper around fetch or axios: exponential backoff retries, timeout handling, normalized error types, maybe a circuit breaker. You rebuild this layer in every project. reixo is that layer — built once, typed thoroughly, and shipped as a single zero-dependency package.
What makes reixo different is its default error-handling model. Instead of throwing exceptions, every request returns a Result<T, E> discriminated union. You cannot accidentally ignore an error — the TypeScript type system enforces that you check result.ok before accessing result.data. This alone eliminates the most common class of HTTP client bugs in production code.
Installation
npm install reixo
# or
yarn add reixo
# or
pnpm add reixoQuick Start
import { HTTPBuilder } from 'reixo'
const client = new HTTPBuilder()
.withBaseURL('https://api.example.com')
.withTimeout(10_000)
.withHeader('Authorization', 'Bearer <your-token>')
.build()
// Result<T, E> style — recommended, no try/catch needed
const result = await client.tryGet<User[]>('/users')
if (result.ok) {
console.log(result.data.data) // User[] — fully typed
} else {
console.error(result.error.status) // HTTPError.status
}
// Traditional throwing style — also supported
const response = await client.get<User[]>('/users')
console.log(response.data)No-Throw Result<T, E> API
The tryGet, tryPost, tryPut, tryDelete methods return Ok | Err instead of throwing. You handle both cases at the call site:
// With axios — exception-based, easy to forget
try {
const res = await axios.get<User[]>('/users')
return res.data
} catch (err) {
if (axios.isAxiosError(err)) {
console.error(err.response?.status) // might be undefined
}
}
// With reixo — Result-based, exhaustive by the type system
const result = await client.tryGet<User[]>('/users')
if (!result.ok) {
console.error(result.error.status) // always defined, always typed
return
}
return result.data.data // TypeScript knows result.data exists hereTyped Error Classes
reixo gives you distinct error classes for each failure mode — no more checking err.response?.status in a catch block:
import {
HTTPError,
NetworkError,
TimeoutError,
AbortError,
CircuitOpenError,
} from 'reixo'
try {
await client.get('/api/data')
} catch (err) {
if (err instanceof HTTPError) {
console.error(`HTTP ${err.status}: ${err.statusText}`)
} else if (err instanceof TimeoutError) {
console.error(`Timed out after ${err.timeoutMs}ms`)
} else if (err instanceof CircuitOpenError) {
console.warn('Circuit breaker is open — using fallback data')
} else if (err instanceof NetworkError) {
console.error('Network failure:', err.message)
}
}Built-in Retry with Exponential Backoff
Configure retries once at the client level — no per-request boilerplate:
const client = new HTTPBuilder()
.withBaseURL('https://api.example.com')
.withRetry({
maxRetries: 3,
initialDelayMs: 200,
backoffFactor: 2, // 200ms, 400ms, 800ms
jitter: true, // Randomize delays to spread concurrent retries
retryCondition: (err) => {
if (err instanceof NetworkError) return true
if (err instanceof HTTPError) return err.status >= 500 || err.status === 429
return false
},
onRetry: (err, attempt, delayMs) => {
console.log(`Retry #${attempt} in ${delayMs}ms due to: ${err.message}`)
},
})
.build()Circuit Breaker
When a downstream service is down, continuing to send requests wastes resources and causes cascading failures. The circuit breaker opens after a configurable failure threshold and stops all requests immediately:
const client = new HTTPBuilder()
.withCircuitBreaker({
failureThreshold: 5, // Open after 5 consecutive failures
resetTimeoutMs: 30_000, // Try to recover after 30 seconds
onStateChange: (prev, next) => {
console.log(`Circuit breaker: ${prev} → ${next}`)
// Possible states: CLOSED, OPEN, HALF_OPEN
},
})
.build()
// When OPEN, requests fail instantly with CircuitOpenError
// — no network call is made, protecting the downstream service
const result = await client.tryGet('/api/orders')
if (!result.ok && result.error instanceof CircuitOpenError) {
return cachedOrders // Fall back to stale data
}Request Deduplication
When multiple components trigger the same GET request simultaneously (common on page load), deduplication collapses them into a single network call:
const client = new HTTPBuilder()
.withBaseURL('https://api.example.com')
.withDeduplication()
.build()
// All 5 calls share one Promise — only 1 HTTP request is made
const [r1, r2, r3, r4, r5] = await Promise.all([
client.get('/config'),
client.get('/config'),
client.get('/config'),
client.get('/config'),
client.get('/config'),
])Deduplication keys on method + URL + query parameters. POST, PUT, and DELETE requests are never deduplicated because they have side effects.
LRU Caching
const client = new HTTPBuilder()
.withCache({
ttl: 120_000, // Cache entries live for 2 minutes
strategy: 'stale-while-revalidate', // Return stale data, refetch in background
storage: 'memory',
maxEntries: 200, // LRU eviction after 200 entries
})
.build()
const res = await client.get('/config')
if (res.cacheMetadata?.hit) {
console.log(`Cache hit — ${res.cacheMetadata.age}s old`)
}OpenTelemetry Tracing — Zero Config
Injects W3C traceparent headers on every outgoing request with no @opentelemetry/* dependencies:
const client = new HTTPBuilder()
.withOpenTelemetry({
serviceName: 'checkout-service',
baggage: { 'user.tier': 'premium' },
})
.build()
// Continuing a distributed trace from an incoming request
import { parseTraceparent } from 'reixo'
const parentCtx = parseTraceparent(req.headers['traceparent'])
const tracedClient = new HTTPBuilder()
.withOpenTelemetry({ parentContext: parentCtx ?? undefined })
.build()Migrating from axios
The reixo API is intentionally similar to axios for easy migration:
// Before — axios
import axios from 'axios'
const api = axios.create({ baseURL: 'https://api.example.com' })
const res = await api.get('/users')
res.data // User[]
// After — reixo (same shape, Result wrapping optional)
import { HTTPBuilder } from 'reixo'
const api = new HTTPBuilder().withBaseURL('https://api.example.com').build()
const res = await api.get<User[]>('/users')
res.data // User[] — identical structureRuntime Support
| Environment | Support |
|---|---|
| Node.js 18+ | yes |
| Bun 1.0+ | yes |
| Deno 1.28+ | yes |
| Cloudflare Workers | yes |
| Vercel Edge | yes |
| Modern Browsers | yes |
Zero dependencies — uses native fetch and AbortController only.
Common Mistakes
Using the throwing API and forgetting try/catch. Either use the try* methods for Result-based handling, or wrap every client.get() call in try/catch. Mixing both styles inconsistently creates coverage gaps.
Not configuring a timeout. Without .withTimeout(), long-running requests block indefinitely. Always set a reasonable timeout (5–30 seconds depending on the operation).
Opening too many circuit breakers per request. Create one shared client per downstream service — each client has its own breaker state.
Best Practices
- Create one
HTTPBuilderclient instance per downstream service and reuse it across the app — this ensures the circuit breaker and deduplication state are shared. - Use the
tryGet/tryPostfamily of methods for all production code — the Result pattern eliminates uncaught promise rejections. - Set
withRetryfor idempotent endpoints andwithCircuitBreakerfor any service that might go down under load. - Combine
withDeduplicationandwithCachefor public read endpoints that many components query on mount. - Use
withOpenTelemetrywith aparentContextparsed from the incoming request headers to propagate distributed traces through your service mesh.
Key Takeaways
- reixo is a zero-dependency TypeScript HTTP client that runs on Node.js, Bun, Deno, Cloudflare Workers, and browsers.
- The
tryGet/tryPostmethods returnResult<T, E>— a discriminated union that forces error handling at the type level without try/catch. - Built-in retry with exponential backoff and jitter prevents thundering herd problems when recovering from server-side failures.
- The circuit breaker opens after a configurable failure threshold and stops requests immediately, preventing cascading failures in distributed systems.
- Request deduplication collapses simultaneous identical GET requests into a single network call — useful on page load when multiple components fetch the same resource.
- LRU caching supports
stale-while-revalidate,cache-first, andnetwork-firststrategies configurable per client. - OpenTelemetry tracing injects W3C
traceparentheaders with no@opentelemetry/*peer dependencies required. - Migrating from axios is straightforward — the response shape (
res.data) is identical, making it a drop-in replacement.
Advertisement