Zero-dependency HTTP client. Built on native fetch. Nothing to compromise.
axios was just compromised via a supply chain attack (March 2026). One of its transitive dependencies was hijacked, putting millions of projects at risk.
reqcraft has zero dependencies — there's nothing to compromise.
The entire source is ~500 lines of TypeScript. You can read and audit it in 5 minutes.
| reqcraft | axios | |
|---|---|---|
| Dependencies | 0 | 8+ transitive |
| Bundle size | ~3kb min | ~13kb min |
| Supply chain risk | None | Demonstrated (Mar 2026) |
| Built on | Native fetch |
XMLHttpRequest |
| TypeScript | First-class, strict | Bolted on |
| Tree-shakeable | Yes | No |
| Full audit time | 5 minutes | Hours |
npm install reqcraft
pnpm add reqcraft
yarn add reqcraft
import { createClient } from "reqcraft"; const api = createClient({ baseURL: "https://api.example.com", headers: { Authorization: "Bearer token" }, timeout: 5000, }); // GET with typed response const { data } = await api.get<User[]>("/users"); // POST — JSON auto-serialized await api.post("/users", { name: "John", email: "[email protected]" }); // PUT await api.put("/users/1", { name: "Jane" }); // DELETE await api.delete("/users/1");
import reqcraft from "reqcraft"; const { data } = await reqcraft.get<User[]>("https://api.example.com/users");
api.get("/search", { params: { q: "reqcraft", page: 1, active: true } }); // GET /search?q=reqcraft&page=1&active=true
const api = createClient({ timeout: 5000 }); // Override per request api.get("/slow-endpoint", { timeout: 15000 });
Uses native AbortController under the hood — no polyfills.
const controller = new AbortController(); api.get("/users", { signal: controller.signal }); controller.abort();
api.get("/flaky-endpoint", { retry: 3, retryDelay: 300, // 300ms, 600ms, 900ms (linear backoff) });
Only retries on 5xx errors, 429 (rate limited), or network failures. Safe by default.
// Add auth header to every request api.interceptors.request.use((config) => { config.headers = { ...config.headers, "X-Request-Id": crypto.randomUUID(), }; return config; }); // Log every response api.interceptors.response.use((response) => { console.log(`${response.config.method} ${response.config.url} → ${response.status}`); return response; }); // Remove an interceptor const id = api.interceptors.request.use(fn); api.interceptors.request.eject(id);
import { isReqcraftError } from "reqcraft"; import type { ReqcraftError } from "reqcraft"; try { await api.get("/not-found"); } catch (err) { if (isReqcraftError(err)) { console.log(err.status); // 404 console.log(err.statusText); // "Not Found" console.log(err.data); // Response body } }
// JSON (default) const { data } = await api.get<User>("/user/1"); // Blob const { data: blob } = await api.get<Blob>("/file.pdf", { responseType: "blob" }); // ArrayBuffer const { data: buffer } = await api.get<ArrayBuffer>("/binary", { responseType: "arraybuffer" }); // Text const { data: html } = await api.get<string>("/page", { responseType: "text" }); // ReadableStream const { data: stream } = await api.get<ReadableStream>("/stream", { responseType: "stream" });
api.get("/large-file", { responseType: "blob", onDownloadProgress: ({ loaded, total, percent }) => { console.log(`${percent}% downloaded (${loaded}/${total} bytes)`); }, });
const api = createClient({ transformRequest: [(data) => { // Modify data before sending return { ...data, timestamp: Date.now() }; }], transformResponse: [(data) => { // Transform response data return data; }], });
const api = createClient({ validateStatus: (status) => status < 500, // treat 4xx as success });
const form = new FormData(); form.append("avatar", fileInput.files[0]); form.append("name", "John"); await api.post("/upload", form); // FormData is auto-detected — no manual content-type needed
import { createClient, usePlugin } from "reqcraft"; import type { ReqcraftPlugin } from "reqcraft"; const logger: ReqcraftPlugin = { name: "logger", install(instance) { instance.interceptors.request.use((config) => { console.log(`→ ${config.method} ${config.url}`); return config; }); instance.interceptors.response.use((response) => { console.log(`← ${response.status}`); return response; }); }, }; const api = createClient({ baseURL: "https://api.example.com" }); usePlugin(api, logger);
Creates a new reqcraft instance with optional defaults.
| Method | Signature |
|---|---|
get |
get<T>(url, config?) |
post |
post<T>(url, body?, config?) |
put |
put<T>(url, body?, config?) |
patch |
patch<T>(url, body?, config?) |
delete |
delete<T>(url, config?) |
head |
head<T>(url, config?) |
options |
options<T>(url, config?) |
| Option | Type | Default | Description |
|---|---|---|---|
baseURL |
string |
— | Prepended to all request URLs |
headers |
Record<string, string> |
— | Default headers |
timeout |
number |
— | Request timeout in ms |
retry |
number |
0 |
Number of retry attempts |
retryDelay |
number |
300 |
Base delay between retries (ms) |
params |
Record<string, string | number | boolean> |
— | URL query parameters |
signal |
AbortSignal |
— | Abort signal for cancellation |
responseType |
"json" | "text" | "blob" | "arraybuffer" | "stream" |
"json" |
Response body type |
validateStatus |
(status: number) => boolean |
s >= 200 && s < 300 |
Define success status codes |
transformRequest |
Function[] |
— | Transform request data pipeline |
transformResponse |
Function[] |
— | Transform response data pipeline |
onDownloadProgress |
Function |
— | Download progress callback |
| Function | Description |
|---|---|
isReqcraftError(error) |
Type guard to check if an error is a ReqcraftError |
usePlugin(instance, plugin) |
Install a plugin on a reqcraft instance |
Check out the Migration Guide — it takes about 2 minutes.
Read our Security Policy to understand why zero dependencies matter.