Every project starts the same way. You reach for axios. Then reality sets in:
npm install axios
# ...then axios-retry for retry logic
# ...then axios-cache-interceptor for caching
# ...then you discover a circuit breaker just doesn't exist
# ...then you wrap every single call in try/catch
# ...then you realize WebSocket, SSE, and GraphQL aren't included either
ky? Lovely, but browser-only by design — no circuit breaker, no offline queue, no GraphQL, no Result.
I got tired of reassembling the same resilience stack on every project, so I built it once: reixo — an HTTP client that ships complete.
npm install reixo
No peer dependencies. Runs unchanged on Node.js 20+, Bun, Deno, Cloudflare Workers, Vercel Edge, and browsers.
The one idea that changed how I write request code
reixo returns errors as values, not exceptions. The try* methods give you a "Result", and TypeScript won't let you touch the data until you've handled the error branch:
import { HTTPClient } from 'reixo';
const client = new HTTPClient({
baseURL: 'https://api.example.com/v1',
retry: true, // 3 retries, exponential backoff, 5xx / 429 / 408
circuitBreaker: true, // open after 5 failures, reset after 30s
cacheConfig: true, // in-memory LRU, 5 min TTL
enableDeduplication: true,
});
const result = await client.tryGet<User>('/users/1');
if (!result.ok) {
return handleError(result.error); // fully typed HTTPError
}
console.log(result.data.name); // TypeScript knows the type here
No try/catch. No "did I remember to handle the 500?" An entire class of runtime errors just disappears.
What "ships complete" actually means
All of this is in the box — zero extra installs:
- Result API — tryGet, tryPost, etc. force you to handle both branches
- Resilience stack — retry w/ backoff, circuit breaker (with half-open probing), rate limiter, offline queue — configured in one place
- Request deduplication — three simultaneous GETs to the same URL share one round-trip
- Caching — LRU memory + localStorage/sessionStorage, cache-first / network-first / stale-while-revalidate
- Real-time transports — typed WebSocket client (reconnect + heartbeat) and SSE client
- GraphQL client — queries, mutations, Automatic Persisted Queries
- Zero-dependency OpenTelemetry — W3C traceparent, no OTEL SDK
- Data utilities — infinite query, cursor pagination, priority task queue with persistence, resumable chunked upload
- Dev ergonomics — fluent HTTPBuilder, MockAdapter for tests, NetworkRecorder for fixtures
How it stacks up
A real example: poll a job until it's done
import { poll } from 'reixo';
const { promise, cancel } = poll(() => client.get<Job>('/jobs/42'), {
interval: 2_000,
until: (res) => res.data.status === 'done',
timeout: 120_000,
});
const result = await promise;
No hand-rolled setInterval, no leak-prone cleanup. Built in.
Try it
npm install reixo
📦 npm: https://www.npmjs.com/package/reixo
⭐ GitHub: https://github.com/webcoderspeed/reixo
It's MIT, TypeScript-first, Node 20+. I'm building in public — if you try it, tell me what your "copy-paste into every project" HTTP setup looks like. That's exactly the gap I'm trying to close.

Top comments (0)