DEV Community

Sanjeev
Sanjeev

Posted on

I rebuilt the HTTP client setup I copy-paste into every project — into one package

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Result API — tryGet, tryPost, etc. force you to handle both branches
  2. Resilience stack — retry w/ backoff, circuit breaker (with half-open probing), rate limiter, offline queue — configured in one place
  3. Request deduplication — three simultaneous GETs to the same URL share one round-trip
  4. Caching — LRU memory + localStorage/sessionStorage, cache-first / network-first / stale-while-revalidate
  5. Real-time transports — typed WebSocket client (reconnect + heartbeat) and SSE client
  6. GraphQL client — queries, mutations, Automatic Persisted Queries
  7. Zero-dependency OpenTelemetry — W3C traceparent, no OTEL SDK
  8. Data utilities — infinite query, cursor pagination, priority task queue with persistence, resumable chunked upload
  9. 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;
Enter fullscreen mode Exit fullscreen mode

No hand-rolled setInterval, no leak-prone cleanup. Built in.

Try it

npm install reixo
Enter fullscreen mode Exit fullscreen mode

📦 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)