DEV Community

Shihiro
Shihiro

Posted on

Is Axios Dead? I Built a Policy-First HTTP Client for TypeScript

Beyond Axios: From Imperative Requests to Declarative Transport Policies with pureq

Most TypeScript teams still start with one default answer for HTTP: "just use Axios".

That was a good answer for years.
But today, the core challenge is no longer sending requests. It is designing transport behavior that stays reliable, observable, and maintainable as systems grow.

That is a paradigm shift:

  • Imperative style (per-call patches and interceptor side effects)
  • Declarative policy style (explicit, composable transport rules)

pureq is built for the second model.

Why Existing Options Start to Hurt

Native fetch: flexible, but incomplete by default

fetch gives you primitives, not a reliability system. Teams usually rebuild:

  • Timeout/deadline behavior
  • Retry rules
  • Circuit breaker logic
  • Deduplication
  • Unified error classification

This often leads to duplicated utilities and inconsistent behavior across services.

Axios: ergonomic, but increasingly implicit at scale

Axios interceptors are useful, but large codebases tend to hit:

  • Hidden side effects
  • Order-dependent behavior that is hard to audit
  • Blurred boundaries between clients (public/auth/admin/internal)

fetch vs Axios vs pureq (At a Glance)

Capability fetch Axios pureq
Immutable client composition No No (instance config is mutable) Yes (use() returns a new client)
Resilience policies (retry/circuit/deadline/dedupe) Manual Partial/custom interceptor logic First-class middleware
Middleware ordering model Manual wrappers Interceptor chains Explicit onion model
Result pattern (non-throwing API) Manual Mostly exception-first Built-in *Result APIs
Observability hooks / OTel mapping Manual Manual Built-in diagnostics + OTel mapping
Runtime dependencies N/A (platform API) External package Zero runtime dependencies

What pureq Is

pureq is a policy-first HTTP transport layer for TypeScript.

Core ideas:

  • Policy-first design
  • Immutable clients
  • Composable middleware stack
  • Result-oriented error handling

Also important in practice:

  • Zero runtime dependencies (Lightweight core, no supply chain bloat)
  • Cross-runtime (Browser, Node.js, Bun, Deno, and Edge)

Quick Start

npm install @pureq/pureq
Enter fullscreen mode Exit fullscreen mode
import { createClient } from "@pureq/pureq";

const api = createClient({
  baseURL: "https://api.example.com",
  headers: {
    "Content-Type": "application/json",
  },
});
Enter fullscreen mode Exit fullscreen mode

Design Highlights

1. Immutable composition

use() does not mutate the existing client.

import { createClient, retry, authRefresh, dedupe } from "@pureq/pureq";

const base = createClient({ baseURL: "https://api.example.com" })
  .use(retry({ maxRetries: 2, delay: 300 }));

const privateApi = base.use(
  authRefresh({
    status: 401,
    refresh: async () => getNewToken(),
  })
);

const publicApi = base.use(dedupe());
Enter fullscreen mode Exit fullscreen mode

This makes policy branching explicit and safe.

2. Explicit middleware order (Onion model)

import { createClient, dedupe, retry, circuitBreaker } from "@pureq/pureq";

const resilientApi = createClient({ baseURL: "https://api.example.com" })
  .use(dedupe())
  .use(
    retry({
      maxRetries: 3,
      delay: 200,
      retryOnStatus: [429, 500, 503],
    })
  )
  .use(
    circuitBreaker({
      failureThreshold: 5,
      cooldownMs: 30_000,
    })
  );
Enter fullscreen mode Exit fullscreen mode

Built-in Capabilities

  • retry
  • circuit breaker
  • dedupe
  • timeout / deadline
  • auth refresh
  • hedged requests
  • concurrency limits
  • HTTP cache
  • offline queue
  • validation / fallback
  • diagnostics and OpenTelemetry mapping

Validation Example (Zod/Valibot Friendly)

pureq ships a zero-dependency validation middleware that can bridge external schema libraries.

import { createClient, validation } from "@pureq/pureq";
import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
});

const api = createClient({ baseURL: "https://api.example.com" }).use(
  validation({
    validate: (data) => UserSchema.parse(data),
    message: "Response validation failed",
  })
);
Enter fullscreen mode Exit fullscreen mode

The same shape works with Valibot validators as well.

Result Pattern: Errors as Values, Not Exceptions

pureq separates transport failures and HTTP failures via typed Result unions.

const result = await api.getJsonResult<User>("/users/:id", {
  params: { id: "42" },
});

if (!result.ok) {
  switch (result.error.kind) {
    case "timeout":
      showToast("Request timed out");
      break;
    case "circuit-open":
      showFallbackUI();
      break;
    case "http":
      if (result.error.status === 401) {
        logout();
      }
      break;
    default:
      reportError(result.error);
  }
  return;
}

// TypeScript narrows here: result is { ok: true; data: User }
renderUser(result.data);
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • Better exhaustiveness and discoverability in code review
  • Fewer hidden throw paths in async call chains
  • Stronger type safety for success/failure handling

Works Well with React Query / SWR

Use pureq for transport policy, then layer state tools on top.

import { useQuery } from "@tanstack/react-query";

function useUser(id: string) {
  return useQuery({
    queryKey: ["user", id],
    queryFn: async () => {
      const result = await api.getJsonResult<User>("/users/:id", {
        params: { id },
      });

      if (!result.ok) {
        throw result.error;
      }

      return result.data;
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Where pureq Fits Best

  • Large frontends with shared transport policy
  • BFF/backends with reliability and observability requirements
  • Multi-runtime deployments including edge environments
  • Teams that need predictable, auditable transport behavior

Where It May Be Overkill

  • Very small apps with minimal HTTP complexity
  • Short-lived prototypes
  • Cases where speed of initial setup matters more than long-term policy consistency

Final Note

pureq is not trying to be "yet another HTTP helper".
It is a transport design model: explicit policies, immutable composition, and typed failure handling.

I actively dogfood pureq in production workloads and keep evolving it based on real incidents and maintenance pressure.

If this aligns with your architecture goals:

Top comments (0)