Skip to content

lahin31/amu-http

Repository files navigation

Amu

npm version npm downloads bundle size types license

Amu is a Fetch-first HTTP client for modern JavaScript and TypeScript apps.

It keeps native Fetch behavior while removing the boilerplate that slows teams down in real-world systems.

Safer URL handling by default: strict URL parsing (syntax-level validation only) rejects malformed absolute URLs instead of silently normalizing them.

Table of Contents

Getting started

Overview & comparison

Making requests

Timeouts, retries & URL safety

Errors & behavior

Advanced

Reference

📦 Installation

npm install amu-http

⚡ Quick Example

// Native Fetch
const res = await fetch('/users');
if (!res.ok) throw new Error('Request failed');
const users = await res.json();

// Axios
const res2 = await axios.get('/users');
const users2 = res2.data;

// Amu
const users3 = await amu.get('/users');

💡 Why Amu

  • Direct data access (no res.data)
  • Deterministic retries (network errors by default; configurable status-code retries)
  • Structured errors (HTTP + network)
  • URL safety (strict URL parsing, syntax-level validation only)
  • Schema validation support
  • Tiny footprint (~1.6KB gzip)

Amu is a thin, opinionated layer over Fetch with explicit, testable behavior:

  • Throws on non-2xx responses (AmuError) instead of returning ok: false responses.
  • Retries only idempotent methods (GET, HEAD) by default.
  • Rejects malformed absolute URLs early (AmuUrlError).

It also has two standout capabilities:

  • Structured Network Errors (AmuNetworkError) for reliable retry/debug logic
  • Built-in Schema Validation for runtime-safe API parsing

⚖️ Amu vs Axios

Feature Amu Axios
Data access Direct (await get()) res.data
Fetch-native ❌ (adapters)
Retry semantics HTTP-aware Manual
Error structure Typed & structured Less structured
URL validation Strict Lenient
TypeScript support Excellent (full inference) Good
Bundle size ~1.6KB (gzip) ~14KB (gzip)

🎯 Killer Features

1) Structured Network Errors

Amu provides a dedicated AmuNetworkError with:

  • kind (network | timeout | abort | unknown)
  • isRetryable
  • cause

This gives you predictable retry and debugging behavior without guessing from generic "Network Error" strings.

2) Built-in Schema Validation

Amu supports validator-driven parsing at the request layer:

  • Zod-style schema support (via .parse interface) + custom validators
  • custom validation functions

You get runtime data-shape guarantees at the boundary where APIs enter your app.

3) Excellent TypeScript Support

Amu is built with TypeScript-first design:

  • Full generic type inference on all methods
  • Method overloads for raw option (returns AmuRawResponse<T> or T)
  • Typed error classes (AmuError, AmuNetworkError, AmuUrlError, AmuValidationError)
  • Complete AmuConfig typing with native Fetch options
  • Schema-driven type inference from Zod & custom validators
// Type inference works seamlessly
const user = await amu.get<User>('/user/1');
const res = await amu.get<User>('/user/1', { raw: true }); // AmuRawResponse<User>

// Schema validation infers types
const userData = await amu.get('/user', { schema: UserSchema });

🧠 Core Features

  • Auto JSON / text parsing
  • Built-in timeout support
  • Retry policies with full control
  • Query params support
  • Schema validation (Zod + custom)
  • Full TypeScript support with type inference
  • Instance-based client factory
  • Tiny footprint for browser & server

⚙️ Built-in Defaults

Amu comes with sensible defaults to make your code safer without extra configuration:

Setting Default Override
Timeout 10 seconds (10000ms) { timeout: 5000 }
Retries 0 (no retries) { retries: 2 } or retry config
Content-Type application/json { headers: { 'Content-Type': '...' } }
Idempotent retries GET, HEAD only { retries: { allowNonIdempotent: true } }
Default retryOn Network errors { retries: { retryOn: [500, 502, 503] } }

🚀 Usage

Basic GET

import amu from 'amu-http';

const users = await amu.get('https://jsonplaceholder.typicode.com/users');

HEAD

import amu from 'amu-http';

// Check if resource exists without downloading the body
await amu.head('https://api.example.com/users/1');

HEAD requests are useful for checking resource availability or metadata without fetching the full response body. Like GET, HEAD is an idempotent method and will be retried by default if configured.


Axios-style Raw Response

If you prefer Axios-like response objects, pass raw: true.

import amu from 'amu-http';

const res = await amu.get('/users', { raw: true });

console.log(res.data); // parsed payload
console.log(res.status); // HTTP status code
console.log(res.statusText); // HTTP status text
console.log(res.headers); // plain header object
console.log(res.config); // resolved request config
console.log(res.request); // native Fetch Response

With schema validation, res.data is still validated:

const res = await amu.get('/user/1', {
  raw: true,
  schema: UserSchema,
});

Response Readers

Amu requests return an AmuPromise with built-in response reader methods. These allow you to access different response body formats without re-fetching:

import amu from 'amu-http';

const promise = amu.get('/users');

// All of these read from the same underlying response
const data = await promise;                    // Parsed data (JSON or text)
const json = await promise.json<User[]>();   // Force JSON parsing
const text = await promise.text();             // Get response as text
const blob = await promise.blob();             // Get response as blob

These reader methods are particularly useful when you need multiple response formats or want to handle parsing errors gracefully:

const promise = amu.get('/data', { debug: true });

try {
  const parsed = await promise;
  console.log('Success:', parsed);
} catch (error) {
  // Fallback to reading as text
  const raw = await promise.text();
  console.log('Raw response:', raw);
}

POST (JSON)

import amu from 'amu-http';

await amu.post('/posts', {
  title: 'hello',
  body: 'from amu',
});

PUT / PATCH / DELETE

import amu from 'amu-http';

await amu.put('/users/1', { name: 'Updated Name' });
await amu.patch('/users/1', { role: 'admin' });
await amu.delete('/users/1');

OPTIONS

import amu from 'amu-http';

// Check available HTTP methods for a resource
await amu.options('/api/users');

The OPTIONS method is useful for discovering the communication options available for a resource. It can be used to check which HTTP methods are allowed by the server.


Query Params

import amu from 'amu-http';

await amu.get('/users', {
  params: { page: 1, limit: 10 },
});

You can also pass query params directly in the URL:

const users = await amu.get('/users?page=1&limit=10');

Mixing URL query + params also works:

await amu.get('/users?page=1', {
  params: { limit: 10 },
});
// Final URL: /users?page=1&limit=10

You can also provide a custom query serializer with paramsSerializer.

await amu.get('/users', {
  params: { page: 1, search: 'John & Doe' },
  paramsSerializer: (params) =>
    Object.entries(params)
      .filter(([, value]) => value != null)
      .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
      .join('&'),
});

Amu's default query behavior is safer: params values override any duplicate keys already present in the URL.


Headers / Auth

import amu from 'amu-http';

await amu.get('/me', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
  credentials: 'include', // include cookies/credentials for cross-site auth
});

Include credentials when the request needs cookies or browser auth data.


Request Cancellation (AbortController)

import amu from 'amu-http';

const controller = new AbortController();
const promise = amu.get('/users', { signal: controller.signal });

controller.abort();
await promise; // throws AmuNetworkError with kind: 'abort'

signal works together with timeout: whichever aborts first cancels the request.


Debug Latency

Set debug: true to print request latency in the console.

import amu from 'amu-http';

await amu.get('/users', { debug: true });
// [amu][latency] GET /users 128ms attempts=1 status=200

debug works on both instance defaults and per-request config.


Timeout & Retries

Amu has a default timeout of 10 seconds (10000ms) for all requests. Override it per-request or set instance defaults:

import amu from 'amu-http';

await amu.get('/stats', {
  timeout: 5000,
  retries: 2,
});

Advanced retry:

await amu.get('/stats', {
  retries: {
    attempts: 3,
    delay: (attempt) => 2 ** attempt * 100,
    retryOn: ['network-error', 429, 500, 502, 503, 504],
  },
});

Retry lifecycle hooks (instance-level + request override):

import amu from 'amu-http';

const api = amu('https://api.example.com', {
  retries: {
    attempts: 3,
    delay: (attempt) => 2 ** attempt * 100,
    retryOn: ['network-error', 429, 500, 502, 503, 504],
  },
  hooks: {
    onRetry(ctx) {
      console.log('retry', ctx.attempt, ctx.reason, ctx.delay);
    },
    onRetryComplete(ctx) {
      console.log('retry-complete', ctx.success, ctx.totalRetries, ctx.totalDuration);
    },
  },
});

await api.get('/stats', {
  hooks: {
    onRetry(ctx) {
      // Request-level hooks override same-named instance hooks.
      console.log('request retry', ctx.attempt);
    },
  },
});

Hook behavior:

  • onRetry fires each time a retry is scheduled (before backoff sleep).
  • onRetryComplete fires once when retry workflow finishes (final success or final failure).
  • onRetryComplete fires only if at least one retry happened.

❌ URL Safety

Amu performs strict URL parsing (syntax-level validation only) and rejects malformed absolute URLs.

await amu.get('https:google.com'); // throws AmuUrlError
await amu.get('https://google.com'); // valid

⚠️ Error Handling

import amu, { AmuError } from 'amu-http';

try {
  await amu.get('/404');
} catch (err) {
  if (err instanceof AmuError) {
    console.log(err.status);
    console.log(err.data);
    console.log(err.headers);
  }
}

🌐 Network Errors

import amu, { AmuNetworkError } from 'amu-http';

try {
  await amu.get('/users');
} catch (err) {
  if (err instanceof AmuNetworkError) {
    console.log(err.kind); // network | timeout | abort | unknown
    console.log(err.isRetryable);
    console.log(err.cause);
  }
}

Failure Behavior Spec

Amu has explicit failure semantics:

  • HTTP 4xx/5xx (e.g. 404, 500)
    Throws AmuError with:

    • status: HTTP status code
    • data: parsed response body (JSON/text/null)
    • headers: response headers
  • Network failure (DNS/offline/unreachable transport)
    Throws AmuNetworkError with:

    • kind: 'network'
    • isRetryable based on retry policy
    • cause: original underlying error
  • Timeout
    Throws AmuNetworkError with:

    • kind: 'timeout'
    • isRetryable: false by default
  • Abort
    Throws AmuNetworkError with:

    • kind: 'abort'
    • isRetryable: false by default
  • Schema validation failure
    Throws AmuValidationError with:

    • data: unvalidated response payload
    • issues: validator-provided issues (if available)
  • Malformed absolute URL (e.g. https:google.com)
    Throws AmuUrlError before request execution.

Retry defaults:

  • Network errors are retryable when configured via retries.
  • HTTP status retries happen only when status codes are listed in retryOn.
  • Retries are idempotent-method-only by default (GET, HEAD) unless allowNonIdempotent: true.

🧪 Schema Validation

import amu from 'amu-http';
import { z } from 'zod';

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

const user = await amu.get('/user/1', {
  schema: User,
});

🏭 Custom Instance

import amu from 'amu-http';

const api = amu('https://api.example.com', {
  timeout: 8000,
  retries: 1,
  headers: {
    'X-App': 'dashboard',
  },
});

await api.get('/me');

📏 Size

  • Amu (gzip): ~1.6 KB
  • Axios (gzip): ~14 KB

Amu is ~9x smaller while keeping essential features for modern runtimes.


🧠 Design Principles

  • Minimal abstraction over Fetch
  • Predictable behavior over magic
  • Correct defaults over configuration
  • Small surface area over feature bloat
  • Production-safe by design

🛠 Development

npm run lint
npm run test
npm run test:watch
npm run test:coverage
npm run build
npm run dev

About

A graceful, ultra-lightweight HTTP client for modern JavaScript and TypeScript.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors