A lightweight, Axios-like HTTP client built on native fetch.
Predictable. Concurrent-safe. Dependency-free.
Most HTTP clients fall into two extremes:
- Axios β powerful but heavy and legacy-oriented
- Ky β lightweight but limited for real-world auth flows
vfetch sits in the middle.
- Built directly on native
fetch - No dependencies
- Handles token refresh + concurrency safely
- Returns predictable, non-throwing responses
npm install vfetch-client
yarn add vfetch-client
pnpm add vfetch-client
bun add vfetch-clientimport { createClient } from "vfetch-client";
const api = createClient({
baseURL: "https://api.example.com",
});
const res = await api.get("/users");
if (res.ok) {
console.log(res.data);
} else {
console.error(res.error);
}vfetch never throws HTTP errors. Everything resolves into a consistent shape:
type VfetchResponse<T> =
| { ok: true; data: T; status: number }
| { ok: false; error: string; status: number };This removes the need for excessive try/catch and keeps control flow predictable.
Typing is completely optional.
// No typing (default)
const res = await api.get("/users");
// Typed (when you know the shape)
const res = await api.get<User[]>("/users");Types improve DX β they are not enforced at runtime.
api.get("/users");
api.post("/users", { name: "John" });
api.put("/users/1", { status: "active" });
api.patch("/users/1", { role: "admin" });
api.delete("/users/1");vfetch handles token injection and deduplicated refresh flows.
If multiple requests fail with 401, only one refresh request runs, while others wait safely.
let token = "initial-token";
const api = createClient({
baseURL: "https://api.example.com",
getToken: () => token,
onRefresh: async () => {
const res = await fetch("https://api.example.com/refresh", {
method: "POST",
});
const data = await res.json();
token = data.accessToken;
return token;
},
onAuthFailure: () => {
console.error("Session expired");
},
});vfetch supports cookie-based auth via the native credentials option.
The default behavior is "same-origin".
This can be configured globally or per request.
const api = createClient({
baseURL: "https://api.example.com",
credentials: "include",
});await api.get("/me", {
credentials: "include",
});For cross-origin requests using credentials: "include", your backend must be explicitly configured to allow them.
You cannot use Access-Control-Allow-Origin: *.
Example backend CORS headers:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://your-frontend.comconst api = createClient({
baseURL: "https://api.example.com",
onRequest: (url, options) => {
console.log("β", options.method, url);
},
onResponse: (url, res, duration) => {
console.log("β", url, `${duration}ms`);
},
onError: (url, err) => {
console.error("β", url, err.error);
},
});No thrown HTTP errors:
const res = await api.get("/users");
if (!res.ok) {
console.error(res.status, res.error);
return;
}
console.log(res.data);Handles:
- 4xx / 5xx
- network failures
- invalid JSON
- timeouts
- aborted requests
const api = createClient({
baseURL: "https://api.example.com",
timeout: 5000,
retry: 2,
retryDelay: 1000,
});Override per request:
await api.get("/heavy", {
retry: 0,
timeout: 10000,
});const controller = new AbortController();
const res = await api.get("/long-task", {
signal: controller.signal,
});
controller.abort();vfetch works cleanly with TanStack Query.
export const getUsersFn = async () => {
return api.get("/users");
};const { data } = useQuery({
queryKey: ["users"],
queryFn: getUsersFn,
});const { data } = useQuery({
queryKey: ["users"],
queryFn: async () => {
return api.get("/users");
},
});export const resendOtpFn = async (identifier: string) => {
return api.post("/auth/request-otp", { identifier });
};const { mutate } = useMutation({
mutationFn: resendOtpFn,
});const { mutate } = useMutation({
mutationFn: async (identifier: string) => {
return api.post("/auth/request-otp", { identifier });
},
});-
Transport layer only No schema validation, no assumptions about backend structure
-
Predictable responses No hidden throws β always
{ ok, data | error } -
Concurrency safety first Token refresh, retries, and interceptors behave correctly under load
-
Minimal & dependency-free Built directly on native
fetch
| Feature | vfetch | Axios | Ky |
|---|---|---|---|
| Built on fetch | β | β | β |
| Zero dependencies | β | β | β |
| Interceptors | β | β | |
| Token refresh flow | β | manual | β |
| TypeScript-first | β | partial | β |
| Response normalization | β | β | partial |
| Lightweight | β | β | β |
- Axios β mature, but heavier and more legacy-oriented
- Ky β minimal, but lacks structured auth + retry control
- vfetch β modern balance with safer concurrency and predictable responses
Contributions are welcome. Please read the Contributing Guide before opening a PR.
MIT License