Skip to content

Gab-codes/vfetch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

29 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

vfetch

A lightweight, Axios-like HTTP client built on native fetch.
Predictable. Concurrent-safe. Dependency-free.


✨ Why vfetch?

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

πŸ“¦ Installation

npm install vfetch-client
yarn add vfetch-client
pnpm add vfetch-client
bun add vfetch-client

πŸš€ Quick Start

import { 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);
}

🧠 Core Response Model

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.


🧩 TypeScript (Optional)

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.


πŸ”§ Request Methods

api.get("/users");

api.post("/users", { name: "John" });

api.put("/users/1", { status: "active" });

api.patch("/users/1", { role: "admin" });

api.delete("/users/1");

πŸ” Authentication & Token Refresh

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");
  },
});

πŸͺ Cookie-Based Authentication (Web)

vfetch supports cookie-based auth via the native credentials option. The default behavior is "same-origin".

This can be configured globally or per request.

Example (Global)

const api = createClient({
  baseURL: "https://api.example.com",
  credentials: "include",
});

Example (Per Request)

await api.get("/me", {
  credentials: "include",
});

Important Note

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.com

πŸ”„ Interceptors

const 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);
  },
});

⚠️ Error Handling

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

⏱️ Retry & Timeout

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,
});

🧡 Abort Requests

const controller = new AbortController();

const res = await api.get("/long-task", {
  signal: controller.signal,
});

controller.abort();

βš›οΈ TanStack Query (React Query)

vfetch works cleanly with TanStack Query.


Queries

Pattern A β€” Clean abstraction

export const getUsersFn = async () => {
  return api.get("/users");
};
const { data } = useQuery({
  queryKey: ["users"],
  queryFn: getUsersFn,
});

Pattern B β€” Inline

const { data } = useQuery({
  queryKey: ["users"],
  queryFn: async () => {
    return api.get("/users");
  },
});

Mutations

Named function

export const resendOtpFn = async (identifier: string) => {
  return api.post("/auth/request-otp", { identifier });
};
const { mutate } = useMutation({
  mutationFn: resendOtpFn,
});

Inline mutation

const { mutate } = useMutation({
  mutationFn: async (identifier: string) => {
    return api.post("/auth/request-otp", { identifier });
  },
});

🧭 Design Philosophy

  • 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


βš–οΈ vfetch vs Axios vs Ky

Feature vfetch Axios Ky
Built on fetch βœ… ❌ βœ…
Zero dependencies βœ… ❌ βœ…
Interceptors βœ… βœ… ⚠️ (hooks only)
Token refresh flow βœ… manual ❌
TypeScript-first βœ… partial βœ…
Response normalization βœ… ❌ partial
Lightweight βœ… ❌ βœ…

Summary

  • 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

Contributing

Contributions are welcome. Please read the Contributing Guide before opening a PR.


πŸ“„ License

MIT License

About

A lightweight, Axios-like HTTP client built on native fetch. Predictable. Concurrent-safe. Dependency-free.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors