reqcraft
v1.0.0
Published
Zero-dependency, 3kb HTTP client built on native fetch. A secure alternative to axios — no supply chain risk.
Maintainers
Readme
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.
Why reqcraft?
| | 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 |
Install
npm install reqcraftpnpm add reqcraftyarn add reqcraftQuick Start
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");Default Instance
import reqcraft from "reqcraft";
const { data } = await reqcraft.get<User[]>("https://api.example.com/users");Features
Query Parameters
api.get("/search", { params: { q: "reqcraft", page: 1, active: true } });
// GET /search?q=reqcraft&page=1&active=trueTimeout
const api = createClient({ timeout: 5000 });
// Override per request
api.get("/slow-endpoint", { timeout: 15000 });Uses native AbortController under the hood — no polyfills.
Request Cancellation
const controller = new AbortController();
api.get("/users", { signal: controller.signal });
controller.abort();Retry with Backoff
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.
Interceptors
// 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);Error Handling
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
}
}Response Types
// 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" });Download Progress
api.get("/large-file", {
responseType: "blob",
onDownloadProgress: ({ loaded, total, percent }) => {
console.log(`${percent}% downloaded (${loaded}/${total} bytes)`);
},
});Transform Request / Response
const api = createClient({
transformRequest: [(data) => {
// Modify data before sending
return { ...data, timestamp: Date.now() };
}],
transformResponse: [(data) => {
// Transform response data
return data;
}],
});Validate Status
const api = createClient({
validateStatus: (status) => status < 500, // treat 4xx as success
});FormData & File Uploads
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 neededPlugins
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);API Reference
createClient(defaults?)
Creates a new reqcraft instance with optional defaults.
HTTP Methods
| 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?) |
Config Options
| 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 |
Utilities
| Function | Description |
|----------|-------------|
| isReqcraftError(error) | Type guard to check if an error is a ReqcraftError |
| usePlugin(instance, plugin) | Install a plugin on a reqcraft instance |
Coming from axios?
Check out the Migration Guide — it takes about 2 minutes.
Security
Read our Security Policy to understand why zero dependencies matter.
