A lightweight fetch wrapper.
npm install @scirexs/fetchy



A lightweight thin fetch wrapper with built-in retry logic, timeout handling, and automatic body parsing. Works in Deno, Node.js, and modern browsers.
- Simple API - Drop-in replacement for native fetch with enhanced capabilities
- Lightweight - Bundle size is ~5KB uncompressed, ~2KB gzipped, zero dependencies
- Native Fetch Compatible - Thin abstraction layer, easy migration back to native fetch
- Promise-like Interface - Chain parsing methods directly on fetch results
- Timeout Support - Configurable request timeouts with automatic cancellation
- Retry Logic - Exponential backoff with Retry-After header support
- Type-Safe - Full TypeScript support with generic type inference
- Bearer Token Helper - Built-in Authorization header management
- Jitter Support - Prevent thundering herd with randomized delays
- Fluent Interface - Class-based API with both instance and static methods
- HTTP Method Shortcuts - Convenient methods for GET, POST, PUT, PATCH, DELETE
bash
npm
npm install @scirexs/fetchyJSR (Deno)
deno add jsr:@scirexs/fetchy
`Quick Start
`ts
import { fetchy, sfetchy, fy } from "@scirexs/fetchy";// Simple GET request with automatic JSON parsing
const user = await fetchy("https://api.example.com/user/1").json();
// Safe error handling - returns null on failure
const data = await sfetchy("https://api.example.com/data").json();
if (data !== null) {
console.log(data);
}
// Fluent API with reusable configuration
const client = fy({
bearer: "token",
timeout: 10,
retry: { maxAttempts: 5 }
});
const posts = await client.get("/posts").json();
`API Reference
$3
Performs an HTTP request with enhanced features. Returns a promise-like object that can be awaited directly or chained with parsing methods.
#### Parameters
-
url: string | URL | Request | null - The request URL (can be null if options.url is provided)
- options: FetchyOptions (optional) - Configuration options#### Returns
FetchyResponse - A promise-like object that extends Promise with the following methods:-
text() → Promise - Parse response as text
- json → Promise - Parse response as JSON
- bytes() → Promise - Parse response as byte array
- blob() → Promise - Parse response as Blob
- arrayBuffer() → Promise - Parse response as ArrayBuffer
- formData() → Promise - Parse response as FormData#### Example
`ts
// Get Response object
const response = await fetchy("https://api.example.com/data");// Chain JSON parsing
const user = await fetchy("https://api.example.com/user").json();
// POST with automatic body serialization
const result = await fetchy("https://api.example.com/create", {
method: "POST",
body: { name: "John", age: 30 },
bearer: "token"
}).json();
// Binary data
const image = await fetchy("https://api.example.com/image.png").bytes();
`$3
Performs an HTTP request with safe error handling. Returns
null on any failure instead of throwing.#### Parameters
Same as
fetchy().#### Returns
FetchySafeResponse - A promise-like object that extends Promise with the same parsing methods as FetchyResponse.-
text() → Promise - Safe text parsing (returns null on error)
- json → Promise - Safe JSON parsing (returns null on error)
- bytes() → Promise - Safe bytes parsing (returns null on error)
- blob() → Promise - Safe blob parsing (returns null on error)
- arrayBuffer() → Promise - Safe buffer parsing (returns null on error)
- formData() → Promise - Safe form data parsing (returns null on error)#### Example
`ts
// Returns null instead of throwing on error
const response = await sfetchy("https://api.example.com/data");
if (response === null) {
console.log("Request failed gracefully");
} else {
const data = await response.json();
}// Safe parsing still available
const data = await sfetchy("https://api.example.com/data").json();
if (data !== null) {
// Handle successful response
}
`$3
Creates a fluent HTTP client with pre-configured options that provides HTTP method shortcuts.
#### Client Methods
`ts
const client = fy(options);// Main fetch method
await client.fetch(url?, options?) // Returns FetchyResponse
// HTTP method shortcuts
await client.get(url?, options?) // GET request, returns FetchyResponse
await client.post(url?, options?) // POST request, returns FetchyResponse
await client.put(url?, options?) // PUT request, returns FetchyResponse
await client.patch(url?, options?) // PATCH request, returns FetchyResponse
await client.delete(url?, options?) // DELETE request, returns FetchyResponse
await client.head(url?, options?) // HEAD request, returns Promise
// Safe mode methods
await client.sfetch(url?, options?) // Returns FetchySafeResponse
await client.sget(url?, options?) // Safe GET, returns FetchySafeResponse
await client.spost(url?, options?) // Safe POST, returns FetchySafeResponse
await client.sput(url?, options?) // Safe PUT, returns FetchySafeResponse
await client.spatch(url?, options?) // Safe PATCH, returns FetchySafeResponse
await client.sdelete(url?, options?) // Safe DELETE, returns FetchySafeResponse
await client.shead(url?, options?) // Safe HEAD, returns Promise
`All methods can be chained with parsing methods:
`ts
await client.get("/users").json();
await client.post("/create").json();
await client.sfetch("/data").text();
`#### Example
`ts
// Instance usage - reuse configuration
const client = fy({
base: "https://api.example.com",
bearer: "token123",
timeout: 10,
retry: { maxAttempts: 3 }
});const user = await client.get("/user").json();
const posts = await client.get("/posts").json();
// POST with body
const result = await client.post("/create", {
body: { name: "John" }
}).json();
// Safe mode
const data = await client.sget("/data").json();
if (data !== null) {
// Handle successful response
}
`$3
Sets global default options for all fetchy instances.
#### Example
`ts
import { setFetchy, fetchy } from "@scirexs/fetchy";// Set global defaults
setFetchy({
timeout: 30,
retry: { maxAttempts: 5 },
bearer: "global-token"
});
// All subsequent requests use these defaults
await fetchy("https://api.example.com/data");
`Configuration
$3
`ts
interface FetchyOptions extends Omit {
// Request URL (allows null url parameter with this option)
url?: string | URL | Request;
// Base URL prepended to request URL (only for string/URL, not Request)
base?: string | URL;
// Request body (auto-serializes JSON objects)
body?: JSONValue | BodyInit;
// Timeout in seconds (default: 15)
timeout?: number;
// Retry configuration (set to false to disable)
retry?: RetryOptions | false;
// Bearer token (automatically adds "Bearer " prefix)
bearer?: string;
// Maximum jitter delay in seconds before request (default: 0)
jitter?: number;
// Use native fetch error behavior (no HTTPStatusError on 4xx/5xx)
native?: boolean;
}interface RetryOptions {
maxAttempts?: number; // Maximum retry attempts (default: 3)
interval?: number; // Base interval in seconds (default: 3)
maxInterval?: number; // Maximum interval cap (default: 30)
retryOnTimeout?: boolean; // Retry on timeout (default: true)
idempotentOnly?: boolean; // Only retry idempotent methods (default: false)
statusCodes?: number[]; // Status codes to retry (default: [500, 502, 503, 504, 408, 429])
respectHeaders?: string[]; // Headers to respect for retry timing
} // (default: ["retry-after", "ratelimit-reset", "x-ratelimit-reset"])
`#### Default Values
`ts
{
timeout: 15, // 15 seconds
jitter: 0, // No jitter delay
native: false, // Throws HTTPStatusError on non-OK status
retry: {
maxAttempts: 3, // 3 retry attempts
interval: 3, // 3 seconds base interval
maxInterval: 30, // 30 seconds maximum interval
retryOnTimeout: true, // Retry on timeout
idempotentOnly: false, // Retry all methods
statusCodes: [500, 502, 503, 504, 408, 429],
respectHeaders: ["retry-after", "ratelimit-reset", "x-ratelimit-reset"]
}
}
`$3
#### Method
- If body is provided without method: defaults to
"POST"
- If Request object is passed: uses its method
- Otherwise: defaults to "GET"#### Headers
The following headers are automatically set if not specified:
- Accept:
application/json, text/plain
- Content-Type: Automatically determined based on body type:
- string, URLSearchParams, FormData, Blob with type: Not set (native fetch handles it)
- JSONValue (objects, arrays, numbers, booleans): application/json
- Blob without type, ArrayBuffer: application/octet-stream
- Authorization: Bearer ${options.bearer} if bearer is providedNote: Headers from Request objects are preserved and merged with option headers.
Error Handling
$3
Thrown when response status is not OK (4xx, 5xx) unless
native: true is set.
`ts
import { fetchy, HTTPStatusError } from "@scirexs/fetchy";try {
await fetchy("https://api.example.com/data");
} catch (error) {
if (error instanceof HTTPStatusError) {
console.error(error.status); // 404
console.error(error.response); // Response object
console.error(error.message); // "404 https://api.example.com/data"
}
}
`$3
Other errors (network failures, timeout, abort) are thrown as standard errors:
-
TypeError: Network error, DNS resolution failure
- DOMException: Timeout or abort via AbortSignal$3
Use
sfetchy() or safe methods to return null instead of throwing:
`ts
// Safe fetch - returns null on any error
const response = await sfetchy("https://api.example.com/data");
if (response === null) {
// Handle error gracefully
}// Safe parsing methods - return null on error
const data = await sfetchy("https://api.example.com/data").json();
if (data !== null) {
// Process data
}
`$3
Set
native: true to disable HTTPStatusError and get native fetch behavior:
`ts
const response = await fetchy("https://api.example.com/data", {
native: true
});
// Returns Response even for 4xx/5xx status codes
if (!response.ok) {
console.error("Request failed");
}
`Compatibility with Native Fetch
Designed for easy migration back to native
fetch if needed, minimizing maintenance risk.
`ts
// If this library is discontinued, simply delete these declarations
// to fall back to native fetch with minimal code changes.
// import { fetchy as fetch, setFetchy } from "@scirexs/fetchy";
// setFetchy({ native: true });const options: RequestInit = { method: "POST", body: "hello" };
const response = await fetch("https://api.example.com/data", options);
`Usage Examples
$3
`ts
import { fetchy, sfetchy } from "@scirexs/fetchy";// GET with automatic JSON parsing
const users = await fetchy("https://api.example.com/users").json();
// POST with JSON body
const result = await fetchy("https://api.example.com/create", {
method: "POST",
body: { name: "John", email: "john@example.com" }
}).json();
// Custom headers
const response = await fetchy("https://api.example.com/data", {
headers: { "X-Custom-Header": "value" }
});
// Using base URL
const data = await fetchy("/users", {
base: "https://api.example.com"
}).json();
`$3
`ts
// Bearer token authentication
const user = await fetchy("https://api.example.com/me", {
bearer: "your-access-token"
}).json();// Custom authorization
const data = await fetchy("https://api.example.com/data", {
headers: { "Authorization": "Basic " + btoa("user:pass") }
}).json();
`$3
`ts
// Custom timeout
const response = await fetchy("https://slow-api.example.com", {
timeout: 30 // 30 seconds
});// Retry with exponential backoff
// Intervals: 3s, 6s, 12s, 24s (capped at maxInterval)
const data = await fetchy("https://api.example.com/data", {
retry: {
maxAttempts: 5,
interval: 3,
maxInterval: 60
}
}).json();
// Retry only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, TRACE)
const result = await fetchy("https://api.example.com/update", {
method: "POST",
retry: { idempotentOnly: true } // Won't retry POST
}).json();
// Disable retry
const response = await fetchy("https://api.example.com/data", {
retry: false
});
`$3
`ts
import { fetchy, sfetchy, HTTPStatusError } from "@scirexs/fetchy";// Default: throws on error
try {
const data = await fetchy("https://api.example.com/data").json();
} catch (error) {
if (error instanceof HTTPStatusError) {
console.error(
HTTP ${error.status}:, error.response);
}
}// Safe mode: returns null
const data = await sfetchy("https://api.example.com/data").json();
if (data === null) {
console.log("Request failed, using default");
}
// Native mode: no HTTPStatusError
const response = await fetchy("https://api.example.com/data", {
native: true
});
if (!response.ok) {
console.error("Request failed with status", response.status);
}
`$3
`ts
import { Fetchy, fy } from "@scirexs/fetchy";// Create reusable client
const api = fy({
base: "https://api.example.com",
bearer: "token",
timeout: 10,
retry: { maxAttempts: 3 }
});
// HTTP method shortcuts
const users = await api.get("/users").json();
const post = await api.get("/posts/1").json();
const created = await api.post("/posts", {
body: { title: "New Post" }
}).json();
const updated = await api.put("/posts/1", {
body: { title: "Updated" }
}).json();
const patched = await api.patch("/posts/1", {
body: { views: 100 }
}).json();
await api.delete("/posts/1");
// Safe methods
const data = await api.sget("/maybe-fails").json();
if (data !== null) {
// Process data
}
// Override instance options per request
const text = await api.get("/readme.txt", {
timeout: 5
}).text();
`$3
#### Jitter for Load Distribution
`ts
// Add randomized delay to prevent thundering herd
const response = await fetchy("https://api.example.com/data", {
jitter: 2, // Random delay up to 2 seconds before each request
retry: { maxAttempts: 3 }
});
`#### Abort Signals
`ts
// Manual abort control
const controller = new AbortController();
const promise = fetchy("https://api.example.com/data", {
signal: controller.signal
});setTimeout(() => controller.abort(), 5000);
try {
await promise;
} catch (error) {
// Aborted after 5 seconds
}
// Combining timeout with manual abort
const controller = new AbortController();
await fetchy("https://api.example.com/data", {
timeout: 10,
signal: controller.signal
});
// Request will abort after 10 seconds OR when controller.abort() is called
`#### Form Data and File Uploads
`ts
// Form data upload
const formData = new FormData();
formData.append("file", blob, "filename.png");
formData.append("name", "example");await fetchy("https://api.example.com/upload", {
method: "POST",
body: formData
});
// URL-encoded form
const params = new URLSearchParams({ key: "value", foo: "bar" });
await fetchy("https://api.example.com/form", {
method: "POST",
body: params
});
`#### Streaming with ReadableStream
`ts
// ReadableStream can be used via Request object
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("chunk 1\n"));
controller.enqueue(new TextEncoder().encode("chunk 2\n"));
controller.close();
}
});const response = await fetchy("https://api.example.com/stream", {
method: "POST",
body: stream
});
`#### Retry-After Header Respect
`ts
// Automatically respects Retry-After, RateLimit-Reset, X-RateLimit-Reset headers
const data = await fetchy("https://api.example.com/rate-limited", {
retry: {
maxAttempts: 5,
interval: 1, // Minimum interval if header not present
respectHeaders: ["retry-after", "ratelimit-reset", "x-rateLimit-reset", "x-my-retry-after"]
}
}).json();
// If response has "X-My-Retry-After: 10", will wait 10 seconds before retry
`$3
`ts
interface ApiResponse {
success: boolean;
data: T;
error?: string;
}interface Todo {
id: number;
title: string;
completed: boolean;
}
const response = await fetchy("https://api.example.com/todos/1")
.json>();
if (response.success) {
console.log(response.data.title); // Fully typed
}
// With safe parsing
const result = await sfetchy("https://api.example.com/todos/1")
.json>();
if (result !== null && result.success) {
console.log(result.data.completed);
}
``MIT