- Published on
Introducing reixo - The TypeScript HTTP Client That Replaces axios
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
The native fetch API is low-level. axios fills some gaps but leaves the hard parts — circuit breaking, typed errors, offline queuing, OpenTelemetry, Result-style error handling — to third-party plugins or custom code.
reixo bundles all of those patterns into one cohesive, zero-dependency TypeScript library. This is its story.
- Installation
- Quick Start
- Why reixo Over axios?
- 1. No-Throw Result<T, E> API
- 2. Typed Error Classes
- 3. Built-in Retry with Exponential Backoff
- 4. Circuit Breaker
- 5. Request Deduplication
- 6. W3C OpenTelemetry Tracing — Zero Config
- 7. Smart Caching
- 8. Auth Token Refresh — Handles Concurrent 401s
- 9. Offline Queue
- 10. WebSocket, SSE, GraphQL Built-in
- Migrating from axios
- Testing with MockAdapter
- Runtime Support
- Conclusion
Installation
npm install reixo
# or
yarn add reixo
# or
pnpm add reixo
Quick Start
import { HTTPBuilder } from 'reixo'
const client = new HTTPBuilder()
.withBaseURL('https://api.example.com')
.withTimeout(10_000)
.withHeader('Authorization', 'Bearer <token>')
.build()
// Traditional throwing style
const response = await client.get<User[]>('/users')
console.log(response.data) // User[]
// No-throw Result<T, E> style — the recommended approach
const result = await client.tryGet<User[]>('/users')
if (result.ok) {
console.log(result.data.data) // User[]
} else {
console.error(result.error.status) // HTTPError.status
}
Why reixo Over axios?
1. No-Throw Result<T, E> API
The biggest quality-of-life feature — tryGet, tryPost, etc. return Ok | Err instead of throwing. No more try/catch tax:
// With axios — try/catch everywhere
try {
const res = await axios.get('/users')
return res.data
} catch (err) {
if (axios.isAxiosError(err)) {
console.error(err.response?.status)
}
}
// With reixo — clean Result pattern
const result = await client.tryGet<User[]>('/users')
if (!result.ok) {
console.error(result.error.status) // Fully typed!
return
}
return result.data.data
2. Typed Error Classes
No more string matching or isAxiosError() checks:
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 open — using fallback')
} else if (err instanceof NetworkError) {
console.error('Network failure:', err.message)
}
}
3. Built-in Retry with Exponential Backoff
const client = new HTTPBuilder()
.withBaseURL('https://api.example.com')
.withRetry({
maxRetries: 3,
initialDelayMs: 200,
backoffFactor: 2,
jitter: true, // 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`)
},
})
.build()
4. Circuit Breaker
Prevents cascading failures when a downstream service is down:
const client = new HTTPBuilder()
.withCircuitBreaker({
failureThreshold: 5, // Open after 5 consecutive failures
resetTimeoutMs: 30_000, // Attempt recovery after 30s
onStateChange: (prev, next) => {
console.log(`Circuit: ${prev} → ${next}`)
},
})
.build()
// When the breaker is OPEN, requests fail immediately with CircuitOpenError
// — without hitting the network
5. Request Deduplication
Five simultaneous identical GET requests collapse into one network call:
const client = new HTTPBuilder()
.withBaseURL('https://api.example.com')
.withDeduplication()
.build()
// All 5 share one Promise — only 1 network request!
const [r1, r2, r3, r4, r5] = await Promise.all([
client.get('/config'),
client.get('/config'),
client.get('/config'),
client.get('/config'),
client.get('/config'),
])
6. W3C OpenTelemetry Tracing — Zero Config
Injects traceparent, tracestate, and baggage headers with no @opentelemetry/* dependencies:
const client = new HTTPBuilder()
.withOpenTelemetry({
serviceName: 'checkout-service',
baggage: { 'user.tier': 'premium' },
})
.build()
// Continuing an incoming trace (in Express/Next.js/Hono)
import { parseTraceparent } from 'reixo'
const parentCtx = parseTraceparent(req.headers['traceparent'])
const tracedClient = new HTTPBuilder()
.withOpenTelemetry({ parentContext: parentCtx ?? undefined })
.build()
7. Smart Caching
const client = new HTTPBuilder()
.withCache({
ttl: 120_000,
strategy: 'stale-while-revalidate',
storage: 'memory',
maxEntries: 200,
})
.build()
const res = await client.get('/config')
if (res.cacheMetadata?.hit) {
console.log(`Cache hit — ${res.cacheMetadata.age}s old`)
}
8. Auth Token Refresh — Handles Concurrent 401s
import { createAuthInterceptor } from 'reixo'
// When multiple requests fail with 401 simultaneously,
// only ONE token refresh is triggered — the rest queue
const authInterceptor = createAuthInterceptor(client, {
getAccessToken: () => localStorage.getItem('access_token'),
refreshTokens: async () => {
const res = await client.post('/auth/refresh', {
refreshToken: localStorage.getItem('refresh_token'),
})
localStorage.setItem('access_token', res.data.accessToken)
return res.data.accessToken
},
shouldRefresh: (err) => err instanceof HTTPError && err.status === 401,
})
client.addRequestInterceptor(authInterceptor)
9. Offline Queue
const client = new HTTPBuilder()
.withOfflineQueue({
storage: 'localStorage', // Persist across page reloads
maxSize: 100,
})
.build()
// Requests queue while offline — drain automatically on reconnect
await client.post('/events', { type: 'click', timestamp: Date.now() })
client.on('queue:drain', () => console.log('All offline requests complete'))
10. WebSocket, SSE, GraphQL Built-in
import { WebSocketClient, SSEClient, GraphQLClient } from 'reixo'
// WebSocket with auto-reconnect
const ws = new WebSocketClient({
url: 'wss://api.example.com/ws',
reconnect: { maxRetries: 10, initialDelayMs: 1_000 },
heartbeat: { interval: 30_000, message: 'ping' },
})
// Server-Sent Events
const sse = new SSEClient({
url: 'https://api.example.com/stream',
headers: { Authorization: 'Bearer token' },
})
// Type-safe GraphQL
const gql = new GraphQLClient('https://api.example.com/graphql')
const { data } = await gql.query<{ user: User }>({
query: `query GetUser($id: ID!) { user(id: $id) { id name } }`,
variables: { id: '1' },
})
Migrating from axios
It's intentionally easy — the API is similar:
// 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)
import { HTTPBuilder } from 'reixo'
const api = new HTTPBuilder().withBaseURL('https://api.example.com').build()
const res = await api.get<User[]>('/users')
res.data // User[] — same shape!
Testing with MockAdapter
import { MockAdapter, HTTPClient, HTTPError } from 'reixo'
import { describe, it, expect } from 'vitest'
const mock = new MockAdapter()
const client = new HTTPClient({ transport: mock.transport })
mock.onGet('/users').reply(200, [{ id: 1, name: 'Alice' }])
mock.onGet('/users/999').reply(404, { error: 'Not found' })
it('fetches users', async () => {
const res = await client.get('/users')
expect(res.status).toBe(200)
})
it('throws on 404', async () => {
await expect(client.get('/users/999')).rejects.toBeInstanceOf(HTTPError)
})
Runtime Support
reixo works everywhere:
| Environment | Support |
|---|---|
| Node.js 18+ | ✅ |
| Bun 1.0+ | ✅ |
| Deno 1.28+ | ✅ |
| Cloudflare Workers | ✅ |
| Vercel Edge | ✅ |
| Modern Browsers | ✅ |
Zero dependencies — just native fetch and AbortController.
Conclusion
reixo is the HTTP client that modern TypeScript applications deserve. It takes all the boilerplate you've been copy-pasting across projects — retries, circuit breaking, deduplication, auth refresh, tracing — and bundles it into one cohesive, fully-typed, zero-dependency library. If you're tired of the axios ecosystem and want something that just works, give reixo a try.
npm install reixo