- Published on
reixo 2.2.2 — The TypeScript HTTP Client That Handles Everything You Forgot to Handle
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
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
- Result<T, E> — No More try/catch Everywhere
- Fluent HTTPBuilder
- Retry with Exponential Backoff and Jitter
- Circuit Breaker
- Request Deduplication
- LRU Caching with Multiple Strategies
- GraphQL Support
- WebSocket and Server-Sent Events
- OpenTelemetry Tracing
- MockAdapter for Testing
- ResumableUploader
- BatchProcessor
- Why reixo Over axios or fetch?
- Conclusion
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?
| Feature | fetch | axios | reixo |
|---|---|---|---|
| TypeScript-first | partial | partial | ✅ native |
| Result<T,E> API | ❌ | ❌ | ✅ |
| Retry with jitter | manual | plugin | ✅ built-in |
| Circuit breaker | manual | manual | ✅ built-in |
| Request deduplication | manual | manual | ✅ built-in |
| LRU cache | manual | manual | ✅ built-in |
| GraphQL | manual | manual | ✅ built-in |
| WebSocket/SSE | separate | separate | ✅ built-in |
| OTel tracing | manual | plugin | ✅ built-in |
| Mock adapter | manual | axios-mock-adapter | ✅ built-in |
| Resumable uploads | manual | manual | ✅ 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.