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 reqcraftpnpm add reqcraftyarn add reqcraftimport { 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: "john@example.com" });
// 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=trueconst 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 neededimport { 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.