Package Exports
- reixo
- reixo/package.json
Readme
reixo
A TypeScript-first HTTP client for Node.js, browsers, and edge runtimes — with retries, circuit breaking, request deduplication, OpenTelemetry tracing, typed error returns, caching, offline queuing, auth token refresh, and more.
Why reixo?
The native fetch API is low-level. Timeouts, retries, error normalisation, token refresh, distributed tracing, and request deduplication require boilerplate that gets duplicated across every project. axios and got fill some gaps but leave 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 that into one zero-dependency library:
- No-throw Result API.
tryGet,tryPost, etc. returnOk | Err— notry/catchrequired. - Automatic retries. Configurable backoff strategies with per-request and per-client defaults.
- Circuit breaker. Fail fast when a downstream is unhealthy; recover automatically.
- Request deduplication. Concurrent identical GET requests collapse to a single network round-trip.
- Offline queue. Requests made while offline are queued and replayed when connectivity returns.
- Response caching. In-memory TTL cache with fine-grained invalidation control.
- Auth token refresh. Automatic token rotation with queue-based refresh — no duplicate refresh calls.
- OpenTelemetry. W3C
traceparent,tracestate, andbaggagepropagation with zero extra packages. - Server-Side Rendering. Forward cookies, auth headers, and trace context from incoming requests.
- Rate limiting. Client-side rate limiting to protect downstream services.
- WebSocket client. Managed WebSocket connections with auto-reconnect and event subscription.
- SSE client. Typed Server-Sent Events with automatic reconnection.
- GraphQL client. Typed query and mutation support built on top of the HTTP client.
- Request pipeline. Composable middleware chain for interceptors and transformations.
- Mock adapter. Deterministic mocking for tests without patching globals.
- Zero dependencies. Runs on Node.js, browsers, Deno, Bun, and edge runtimes.
Installation
npm install reixoQuick start
import { Reixo } from 'reixo';
const client = Reixo.HTTPBuilder.create('https://api.example.com')
.withRetry({ maxAttempts: 3 })
.withTimeout(5000)
.build();
const users = await client.get<User[]>('/users');
const user = await client.post<User>('/users', { name: 'Alice' });Core API
HTTP methods
const client = Reixo.HTTPBuilder.create('https://api.example.com').build();
await client.get<User>('/users/1');
await client.post<User>('/users', { name: 'Alice' });
await client.put<User>('/users/1', { name: 'Alice Smith' });
await client.patch<User>('/users/1', { name: 'Alice S.' });
await client.delete('/users/1');Result API — no-throw error handling
const result = await client.tryGet<User>('/users/1');
if (result.ok) {
console.log(result.data.name);
} else {
console.error(result.error.status, result.error.message);
}No try/catch. The Ok | Err type forces you to handle errors explicitly at the call site.
Retries
const client = Reixo.HTTPBuilder.create('https://api.example.com')
.withRetry({
maxAttempts: 4,
delay: 200, // Initial delay in ms
backoff: 'exponential', // 'fixed' | 'exponential' | 'linear'
retryOn: [408, 429, 500, 502, 503, 504],
})
.build();Circuit breaker
const client = Reixo.HTTPBuilder.create('https://api.example.com')
.withCircuitBreaker({
failureThreshold: 5, // Open after 5 consecutive failures
recoveryTimeout: 30_000, // Attempt recovery after 30s
})
.build();When the circuit is open, requests fail immediately without hitting the network, giving downstream services time to recover.
Request deduplication
const client = Reixo.HTTPBuilder.create('https://api.example.com').withDeduplication().build();
// These three calls fire simultaneously but produce only one network request
const [a, b, c] = await Promise.all([
client.get('/users/1'),
client.get('/users/1'),
client.get('/users/1'),
]);Offline queue
const client = Reixo.HTTPBuilder.create('https://api.example.com')
.withOfflineQueue({ maxSize: 100 })
.build();
// Requests made while offline are queued and replayed when connectivity returns
await client.post('/events', { type: 'click', target: 'buy-button' });Auth token refresh
import { createAuthInterceptor } from 'reixo';
const client = Reixo.HTTPBuilder.create('https://api.example.com')
.withInterceptor(
createAuthInterceptor({
getToken: () => localStorage.getItem('access_token'),
refreshToken: async () => {
const res = await fetch('/auth/refresh', { method: 'POST' });
const { accessToken } = await res.json();
localStorage.setItem('access_token', accessToken);
return accessToken;
},
})
)
.build();Concurrent 401 responses trigger a single refresh, then replay all queued requests with the new token.
OpenTelemetry tracing
const client = Reixo.HTTPBuilder.create('https://api.example.com')
.withTracing({
serviceName: 'checkout-service',
propagate: ['traceparent', 'tracestate', 'baggage'],
})
.build();W3C trace headers are injected automatically — no @opentelemetry/* packages required.
Response caching
const client = Reixo.HTTPBuilder.create('https://api.example.com')
.withCache({ ttl: 60_000, maxSize: 500 })
.build();
await client.get('/config'); // Network request
await client.get('/config'); // Served from cache (< 60s old)
await client.invalidate('/config'); // Manual invalidationServer-side rendering
import { createSSRClient } from 'reixo';
// In your SSR handler
export async function getServerSideProps(context) {
const client = createSSRClient('https://api.example.com', context.req);
// Forwards cookies, authorization, and trace headers from the incoming request
const data = await client.get('/user/profile');
return { props: { data } };
}Task queue
const queue = new Reixo.TaskQueue({ concurrency: 5 });
const results = await Promise.all(userIds.map((id) => queue.add(() => client.get(`/users/${id}`))));
// At most 5 requests in-flight at any timeWebSocket client
const ws = new Reixo.WebSocketClient({
url: 'wss://realtime.example.com/events',
autoConnect: true,
reconnect: { maxAttempts: 10, delay: 1000 },
});
ws.on('message', (data) => console.log('Received:', data));
ws.send({ type: 'subscribe', channel: 'orders' });SSE client
const sse = new Reixo.SSEClient({ url: 'https://api.example.com/stream' });
sse.on('update', (event) => console.log(event.data));
sse.connect();GraphQL client
const gql = new Reixo.GraphQLClient({ url: 'https://api.example.com/graphql' });
const { user } = await gql.query<{ user: User }>(
`
query GetUser($id: ID!) {
user(id: $id) { id name email }
}
`,
{ id: '1' }
);Request pipeline
const client = Reixo.HTTPBuilder.create('https://api.example.com')
.use(async (req, next) => {
req.headers.set('X-Request-Id', crypto.randomUUID());
const res = await next(req);
console.log(`${req.method} ${req.url} → ${res.status}`);
return res;
})
.build();Mock adapter (testing)
import { Reixo } from 'reixo';
const client = Reixo.HTTPBuilder.create('https://api.example.com').withMock().build();
client.mock.get('/users/1', { id: 1, name: 'Alice' });
const user = await client.get<User>('/users/1');
// Returns { id: 1, name: 'Alice' } — no network callFeature comparison
| Feature | reixo | axios | got | ky | fetch |
|---|---|---|---|---|---|
| TypeScript types (built-in) | Yes | Yes | Yes | Yes | Partial |
| Zero dependencies | Yes | No | No | No | — |
| Automatic retries | Yes | No | Yes | Yes | No |
| Circuit breaker | Yes | No | No | No | No |
| Request deduplication | Yes | No | No | No | No |
| Offline queue | Yes | No | No | No | No |
| Result API (no-throw) | Yes | No | No | No | No |
| Auth token refresh | Yes | via interceptor | No | No | No |
| Response caching | Yes | No | No | No | No |
| OpenTelemetry (W3C) | Yes | No | No | No | No |
| Server-side rendering helpers | Yes | No | No | No | No |
| Rate limiting (client-side) | Yes | No | No | No | No |
| WebSocket client | Yes | No | No | No | No |
| SSE client | Yes | No | No | No | No |
| GraphQL client | Yes | No | No | No | No |
| Request pipeline / middleware | Yes | Yes | Yes | Yes | No |
| Mock adapter | Yes | via axios-mock-adapter | No | No | No |
| Works in browser, Node, Deno, Bun | Yes | Partial | No | Yes | Yes |
Performance
The right comparison: reixo vs real HTTP clients
Comparing reixo to bare native fetch is the wrong frame — bare fetch does nothing: no retry, no timeout management, no error normalisation, no interceptors, no circuit breaking. You can't ship bare fetch in production. The real question is how reixo stacks up against actual competitors.
Published benchmark data (simple GET, mocked responses):
| Client | ops/sec | reixo advantage |
|---|---|---|
| got | ~40,000 | reixo +83% faster |
| axios | ~50,000 | reixo +47% faster |
| node-fetch | ~60,000 | reixo +22% faster |
| reixo (basic) | ~73,000 | (baseline) |
| ky | ~80,000 | within 10%, far more features |
reixo beats or matches every mainstream HTTP library while shipping circuit breaking, SWR caching, deduplication, OTel tracing, offline queue, and typed errors in one zero-dependency package.
reixo vs native fetch (overhead analysis):
Benchmark on Node.js v22, mocked fetch (measures pure client overhead):
| Client | ops/sec | p99 latency | vs native fetch |
|---|---|---|---|
| native fetch | ~130,000 | 25µs | (baseline) |
| reixo (basic) | ~73,000 | 45µs | −44% vs native |
| reixo + retry | ~73,000 | 48µs | −44% vs native |
| reixo + circuit-breaker | ~73,000 | 44µs | −44% vs native |
The ~6µs overhead vs bare fetch is the floor for any correct async HTTP client — it covers two unavoidable await microtask roundtrips (request interceptors + transport), plus AbortController lifecycle, retry resolution, and deduplication checks. Every async HTTP library (ky, axios, got) has the same structural gap.
At real-world network latencies (10–200ms), this 6µs represents 0.003–0.06% of total request time. It is not measurable in production.
How reixo stays fast on the hot path:
- Pre-computed base headers normalised once in constructor, not per request
- Slim transport-config template (2 fields) replaces full
configspread (22+ fields) per call - Response interceptors short-circuit synchronously when none registered (saves one microtask roundtrip — the dominant cost in any async client)
- Progress handler closures allocated lazily — skipped entirely when no progress callbacks are configured (~99% of requests)
- Event payload objects guarded by
hasListeners()— zero allocation forrequest:start/response:successevents when no listeners registered - Incremental request IDs replace
crypto.randomUUID()(~10× cheaper) retryPoliciesscanned once at startup; per-URL.find()skipped on every request when no policies are set
To reproduce: node benchmarks/run.mjs
Bundle size
reixo has zero runtime dependencies. The total minified + gzipped size is available at bundlephobia.
Contributing
git clone https://github.com/webcoderspeed/reixo.git
cd reixo
npm install
npm testPull requests are welcome. For significant changes, open an issue first.
License
MIT