Universal HTTP fetching library β framework-agnostic, SSR-safe, TypeScript-first, zero runtime dependencies.
Created and maintained by Anwar Ramadan (AR-Coder Company)
Built entirely on native
fetch. Inspired by Axios but designed for the modern web. From a classic<script>tag or jQuery application to a Next.js Server Components setup.
Explore detailed guides, interactive examples, and full API references on our official docs site.
- π Zero dependencies β Only uses modern, native
fetch. Compatible with Node 18+, browsers, Edge Runtime, Bun, and Deno. - π Interceptors β Axios-style request, response, and error middleware chains.
- β‘ Smart Retry β Built-in exponential backoff with jitter and customizable status code handling.
- ποΈ Advanced Caching β Memory and LocalStorage backends, TTL-based eviction, and automatic request deduplication (no redundant network calls).
- π Auth Management β Effortless Bearer token injection with a fully integrated 401 token refresh flow.
- β Cancellation β Seamless AbortController-based request timeouts and manual cancellation without leaks.
- π Plugin System β Highly extensible architecture. Extend the client safely without modifying the core.
- βοΈ React / Next.js Ready β Includes
useRequest()anduseMutation()hooks. - π’ Vue / Nuxt Ready β Includes
useApi()anduseApiMutation()composables. - π¦ UMD Build β Drop-in global via CDN, fully compatible with legacy stacks like jQuery.
- π Isomorphic & SSR-safe β Runs perfectly during SSR without browser-specific polyfills or memory leaks.
npm install @ar-coder/xfetch
# or
pnpm add @ar-coder/xfetch
# or
yarn add @ar-coder/xfetchUsing via CDN (UMD browser build):
<script src="https://cdn.jsdelivr.net/npm/@ar-coder/xfetch/dist/xfetch.umd.js"></script>Creating an instance allows you to encapsulate base URLs, default headers, and global configurations such as cache and retry strategies.
import { createClient } from "@ar-coder/xfetch";
const api = createClient({
baseURL: "https://api.example.com",
headers: {
Accept: "application/json",
},
});
// GET with automatic TypeScript inference
const { data } = await api.get<User[]>("/users");
// POST β objects are automatically serialized to JSON
await api.post("/users", { name: "Anwar", role: "admin" });
// Seamless REST support
await api.put("/users/1", { name: "Updated" });
await api.patch("/users/1", { active: false });
await api.delete("/users/1");XFetch exposes custom hooks directly from xfetch/react. These hooks are strictly typed and handle loading/error states out of the box.
import { createClient } from "@ar-coder/xfetch";
import { useRequest, useMutation } from "@ar-coder/xfetch/react";
const api = createClient({ baseURL: "https://api.example.com" });
function UserList() {
const { data, loading, error, execute } = useRequest<User[]>(api, "/users");
if (loading) return <div>Loading...</div>;
if (error)
return <div onClick={execute}>Error: {error.message} - Retry?</div>;
return (
<ul>
{data?.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
function CreateUser() {
const { mutate, loading } = useMutation<User, CreateUserInput>(
api,
"post",
"/users",
);
return (
<button onClick={() => mutate({ name: "Anwar", email: "me@example.com" })}>
{loading ? "Creating..." : "Create User"}
</button>
);
}XFetch exposes composables from xfetch/vue. They automatically integrate with Vue's reactivity system.
<script setup lang="ts">
import { ref } from "vue";
import { createClient } from "@ar-coder/xfetch";
import { useApi, useApiMutation } from "@ar-coder/xfetch/vue";
const api = createClient({ baseURL: "/api" });
// Standard reactive data fetching
const { data, loading, error, execute } = useApi<User[]>(api, "/users");
// Watch reactive properties and auto-refetch
const page = ref(1);
const { data: pagedData } = useApi<User[]>(api, "/users", {
watchSources: [page],
params: { page: page.value },
});
// Mutations
const { mutate: createUser } = useApiMutation<User, CreateUserInput>(
api,
"post",
"/users",
);
</script>XFetch distinguishes itself by seamlessly executing on the server without memory leaks or missing global object errors (window is not defined).
Next.js (App Router / Server Components):
import { createClient } from "@ar-coder/xfetch";
const api = createClient({ baseURL: "https://api.example.com" });
export default async function Page() {
// `cache` and other node-specific deduplication methods work inherently
const { data: users } = await api.get<User[]>("/users");
return <UserList users={users} />;
}Nuxt 3 UseAsyncData:
const api = createClient({ baseURL: "/api" });
// useAsyncData ensures the fetch isn't duplicated on the client-side swap
const { data } = await useAsyncData("users", () =>
api.get<User[]>("/users").then((res) => res.data),
);XFetch is exported over XFetch global object when imported through a <script> tag. It works flawlessly in older environments like jQuery projects!
<script src="https://cdn.jsdelivr.net/npm/@ar-coder/xfetch/dist/xfetch.umd.js"></script>
<script>
const api = XFetch.createClient({ baseURL: "https://api.example.com" });
// Use as replacement for $.ajax
$("#load-btn").on("click", async function () {
try {
const { data } = await api.get("/users");
$("#list").html(data.map((u) => `<li>${u.name}</li>`).join(""));
} catch (err) {
console.error("Request failed: ", err.status);
}
});
</script>Like Axios, you can intercept requests or responses before they are handled by then or catch.
// 1. Add headers before every request
api.interceptors.request.use((ctx) => {
ctx.headers["X-Request-ID"] = crypto.randomUUID();
return ctx;
});
// 2. Transform the response or track analytical metrics
api.interceptors.response.use((res) => {
console.log(
`[${res.status}] ${res.request.url} matched cache:`,
res.fromCache,
);
return res;
});
// 3. Centralized error handling
api.interceptors.error.use((err) => {
if (err.status === 403) window.location.href = "/login";
return err; // re-throw so the local catch block still works
});Stop waiting on redundant data using robust integrated caching. Two modes exist: memory (default) and localStorage (persists cross-tab).
const api = createClient({
baseURL: "https://api.example.com",
// Setup global caching
cache: {
storage: "memory",
ttl: 5 * 60 * 1000, // Cache lives for 5 minutes
},
});
// Force fetching logic per-request:
await api.get("/always-fresh", { cache: false });
await api.get("/use-local-storage", {
cache: { storage: "localStorage", ttl: 3600000 },
});Flaky network connection? Setup exponential backoff retries explicitly.
const api = createClient({
baseURL: "https://api.example.com",
retry: {
count: 3, // Try 3 total times (1 initial + 3 retries = 4 max requests)
delay: 500, // Exponentially wait: 500ms -> 1000ms -> 2000ms
maxDelay: 5000,
statusCodes: [408, 429, 500, 502, 503, 504], // Only retry on safe errors
},
});
// Or disable for an explicit request:
await api.post("/transaction/process", { amount: 50 }, { retry: false });Instead of injecting your tokens via interceptor manually every time, use the powerful built-in auth manager with a native refresh flow implementation.
const api = createClient({
baseURL: "https://api.example.com",
auth: {
token: null, // Initially unauthenticated
// Intercepts 401 unauthorized errors, pauses queue, refreshes, resolves, and plays retry
refreshToken: async () => {
const res = await fetch("/api/auth/refresh", { method: "POST" });
const json = await res.json();
return json.token; // Pass new token
},
},
});
// Set state immediately when login is complete:
api.setAuth("my_new_oauth_token_123");
// Cleans queue + interceptor on logout:
api.clearAuth();XFetch ships with production-grade security hardening out of the box. Every feature is opt-out rather than opt-in β you're safe by default.
Every URL is validated against a blocklist of dangerous protocols before any network call is made. This prevents Server-Side Request Forgery and local file reads.
// These will all throw immediately β no network call is ever made:
api.get("file:///etc/passwd"); // β Blocked: file:
api.get("javascript:alert(1)"); // β Blocked: javascript:
api.get("data:text/html,<script>..."); // β Blocked: data:
api.get("gopher://internal-service"); // β Blocked: gopher:
// Use validateURL directly in your own code:
import { validateURL } from "@ar-coder/xfetch";
validateURL(userProvidedURL); // throws if unsafeWhen debug mode is active, XFetch automatically redacts sensitive headers and embedded URL credentials from all log output. Your tokens will never appear in the browser console or terminal.
// What you see in the console (dev mode):
[XFetch] β GET https://***:***@api.example.com/users
Headers: { authorization: '[REDACTED]', cookie: '[REDACTED]', accept: 'application/json' }
mergeHeaders uses a null-prototype result object and explicitly blocks dangerous keys:
// These keys are silently ignored β they cannot pollute Object.prototype:
mergeHeaders({ "__proto__": "...", "constructor": "..." }); // safe βRequests that carry Authorization, Cookie, or X-Auth-Token headers are never cached β regardless of your cache configuration. This prevents cross-user data leakage in SSR environments or shared memory on the server.
// Even with global cache enabled, authenticated requests always hit the network:
const api = createClient({
baseURL: "https://api.example.com",
cache: { storage: "memory", ttl: 60_000 },
auth: { token: "my-token" },
});
// This request will NOT be cached β auth header is present:
await api.get("/profile");By default, retries only happen for safe, idempotent HTTP methods (GET, HEAD, OPTIONS, PUT, DELETE). Non-idempotent methods (POST, PATCH) are never retried without explicit opt-in, preventing duplicate writes or double charges.
const api = createClient({
retry: {
count: 3,
statusCodes: [408, 500, 502, 503, 504],
// β οΈ Explicitly opt-in to POST retries (user assumes responsibility):
allowedMethods: ["GET", "HEAD", "OPTIONS", "POST"],
},
});
// POST is retried ONLY when allowedMethods includes 'POST'.
// By default, this throws immediately on the first failure β no retries.
await api.post("/payment/charge", { amount: 99.99 });An absolute hard cap of 10 retry attempts is enforced regardless of configuration.
Protect your backend APIs from accidental request floods using the built-in RateLimiter. Ideal for preventing React render-loop or reactive-watcher bugs from hammering your API.
import { createClient, RateLimiter } from "@ar-coder/xfetch";
const api = createClient({ baseURL: "https://api.example.com" });
// Allow max 30 requests every 10 seconds
const limiter = new RateLimiter({ maxRequests: 30, windowMs: 10_000 });
api.interceptors.request.use((ctx) => {
limiter.check(); // throws XFetchError { status: 429 } if limit exceeded
return ctx;
});
// Check remaining budget:
console.log(limiter.remaining); // e.g. 27The client configuration object is frozen with Object.freeze() after initialization. This prevents any runtime mutation that could silently change security-critical settings like baseURL or auth.token.
const config = { baseURL: "https://api.example.com" };
const api = createClient(config);
// This silently fails (strict mode throws TypeError):
config.baseURL = "https://evil.attacker.com"; // β frozen β no effectWe welcome community contributions constraints via Pull Requests. Please see our CONTRIBUTING.md guidelines for information.
- Clone the repo:
git clone https://github.com/anwararcoder/XFetch.git - Install dependencies:
npm install - Make changes in a new branch:
git checkout -b fix-auth - Run validation scripts:
npm run lint # Code styling correctness npm run typecheck # TS compiler validation npm run test # Execute 170+ Vitest specifications
- Submit a pull request!
Anwar Ramadan is a Senior Software Engineer passionate about open-source and modern web architectures. This project is maintained under the umbrella of AR-Coder Company, dedicated to building precise, production-grade developer tooling.
- GitHub: @anwararcoder
- Company: AR-Coder Company
Released under the MIT License. Copyright Β© 2026 Anwar Ramadan - AR-Coder Company.