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.
Getting started
Overview & comparison
Making requests
- Overview
- Basic GET
- HEAD
- Axios-style Raw Response
- Response Readers
- POST (JSON)
- PUT / PATCH / DELETE
- OPTIONS
- Query Params
- Headers / Auth
- Request Cancellation (AbortController)
- Debug Latency
Timeouts, retries & URL safety
Errors & behavior
Advanced
Reference
npm install amu-http// 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');- 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 returningok: falseresponses. - 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
| 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) |
Amu provides a dedicated AmuNetworkError with:
kind(network | timeout | abort | unknown)isRetryablecause
This gives you predictable retry and debugging behavior without guessing from generic "Network Error" strings.
Amu supports validator-driven parsing at the request layer:
- Zod-style schema support (via
.parseinterface) + custom validators - custom validation functions
You get runtime data-shape guarantees at the boundary where APIs enter your app.
Amu is built with TypeScript-first design:
- Full generic type inference on all methods
- Method overloads for
rawoption (returnsAmuRawResponse<T>orT) - Typed error classes (
AmuError,AmuNetworkError,AmuUrlError,AmuValidationError) - Complete
AmuConfigtyping 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 });- 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
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] } } |
import amu from 'amu-http';
const users = await amu.get('https://jsonplaceholder.typicode.com/users');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.
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 ResponseWith schema validation, res.data is still validated:
const res = await amu.get('/user/1', {
raw: true,
schema: UserSchema,
});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 blobThese 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);
}import amu from 'amu-http';
await amu.post('/posts', {
title: 'hello',
body: 'from amu',
});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');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.
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=10You 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:
paramsvalues override any duplicate keys already present in the URL.
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.
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.
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=200debug works on both instance defaults and per-request config.
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:
onRetryfires each time a retry is scheduled (before backoff sleep).onRetryCompletefires once when retry workflow finishes (final success or final failure).onRetryCompletefires only if at least one retry happened.
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'); // validimport 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);
}
}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);
}
}Amu has explicit failure semantics:
-
HTTP 4xx/5xx (e.g. 404, 500)
ThrowsAmuErrorwith:status: HTTP status codedata: parsed response body (JSON/text/null)headers: response headers
-
Network failure (DNS/offline/unreachable transport)
ThrowsAmuNetworkErrorwith:kind: 'network'isRetryablebased on retry policycause: original underlying error
-
Timeout
ThrowsAmuNetworkErrorwith:kind: 'timeout'isRetryable: falseby default
-
Abort
ThrowsAmuNetworkErrorwith:kind: 'abort'isRetryable: falseby default
-
Schema validation failure
ThrowsAmuValidationErrorwith:data: unvalidated response payloadissues: validator-provided issues (if available)
-
Malformed absolute URL (e.g.
https:google.com)
ThrowsAmuUrlErrorbefore 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) unlessallowNonIdempotent: true.
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,
});import amu from 'amu-http';
const api = amu('https://api.example.com', {
timeout: 8000,
retries: 1,
headers: {
'X-App': 'dashboard',
},
});
await api.get('/me');- Amu (gzip): ~1.6 KB
- Axios (gzip): ~14 KB
Amu is ~9x smaller while keeping essential features for modern runtimes.
- Minimal abstraction over Fetch
- Predictable behavior over magic
- Correct defaults over configuration
- Small surface area over feature bloat
- Production-safe by design
npm run lint
npm run test
npm run test:watch
npm run test:coverage
npm run build
npm run dev