Skip to content
GorshipiskpPublic

About

A strongly-typed, middleware-driven HTTP client for TypeScript and JavaScript. Includes built-in retry strategies, automatic `Retry-After` handling, and flexible request middleware support.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

BestFetch

A lightweight, extensible HTTP client built on top of the native fetch API.
Provides retry logic, timeouts, abort handling, and a plugin system — while keeping a simple and predictable interface.


Features

  • Native fetch wrapper (no heavy dependencies)
  • Built-in retry mechanism with exponential backoff
  • Timeout support via AbortController
  • AbortSignal merging
  • Plugin system (request / response / error hooks)
  • Typed responses via generics
  • Simple and flexible API
  • Query parameter support

Installation

npm install bestfetch-g

Basic Usage

import { BestFetch } from "bestfetch-g";

const api = new BestFetch({
    baseURL: "https://api.example.com"
});

const users = await api.get<User[]>("/users");

Typing Responses

You can strongly type the response using generics:

type User = {
    id: number;
    name: string;
};

const users = await api.get<User[]>("/users");

// `users` is auto typed as `User[]`

For more control:

const result = await api.get<{ data: User[] }>("/users", {
    callbacks: {
        onSuccess: (data) => data.data
    }
});

HTTP Methods

GET

await api.get("/users");

POST

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

PUT

await api.put("/users/1", {
    name: "Updated"
});

PATCH

await api.patch("/users/1", {
    name: "Patched"
});

DELETE

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

Query Parameters

await api.get("/users", {
    query: {
        limit: 10,
        offset: 20,
        search: "john doe",
        tag: ["admin", "active"]
    }
});

Generated URL:

/users?limit=10&offset=20&search=john%20doe

Headers

await api.get("/users", {
    headers: {
        Authorization: "Bearer token"
    }
});

Request Body

JSON is automatically serialized:

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

Timeout

await api.get("/users", {
    timeout: 2000 // ms
});

Abort Requests

const controller = new AbortController();

setTimeout(() => controller.abort(), 1000);

await api.get("/users", {
    signal: controller.signal
});

Supports merging multiple signals internally.


Retry Configuration

const api = new BestFetch("https://api.example.com", {
    retry: {
        retries: 5,
        baseDelay: 500
    }
});

Per-request override:

await api.get("/users", {
    retry: {
        retries: 2
    }
});

Callbacks

onSuccess

await api.get("/users", {
    callbacks: {
        onSuccess: (data) => data.users
    }
});

onError (HTTP errors during retry)

Return false to stop retrying. Any other return value leaves the default retry rules in charge.

await api.get("/users", {
    callbacks: {
        onError: (response, isLastAttempt) => {
            if (response.status === 404) return false;
        }
    }
});

After all attempts fail, the client throws HttpError:

import { BestFetch, HttpError } from "bestfetch-g";

try {
    await api.get("/missing");
} catch (error) {
    if (error instanceof HttpError) {
        console.log(error.status, error.body);
    }
}

ParseError

Invalid JSON on a successful status code throws ParseError (with response and cause).

Situation Error type
HTTP 4xx/5xx HttpError
Invalid JSON body ParseError
Network / abort native Error / DOMException

onNetworkError

Same contract as onError: false stops retrying.

await api.get("/users", {
    callbacks: {
        onNetworkError: (error, isLastAttempt) => {
            console.error(error, isLastAttempt);
        }
    }
});

Custom transport

For tests or a custom stack, pass your own Transport:

import { BestFetch, type Transport } from "bestfetch-g";

const transport: Transport = {
    async send(request) {
        return fetch(request);
    }
};

const api = new BestFetch({
    baseURL: "https://api.example.com",
    transport
});

Plugins (Middleware)

Plugins allow you to hook into request/response lifecycle.

Remove a plugin with api.eject(plugin).

Example: Auth Token

import { BestFetch, createAuthPlugin } from "bestfetch-g";

api.use(createAuthPlugin(() => localStorage.getItem("jwt")));

Or inline:

api.use({
    onRequest(request) {
        const token = localStorage.getItem("jwt");
        if (!token) return request;

        const headers = new Headers(request.headers);
        headers.set("Authorization", `Bearer ${token}`);
        return new Request(request, { headers });
    }
});

Example: Logging

api.use({
    onRequest(request) {
        console.log("Request:", request.url);
        return request;
    },
    onResponse(response) {
        console.log("Response:", response.status);
        return response;
    }
});

Example: Global Error Handling

api.use({
    onError(error) {
        console.error("Global fetch error:", error);
        return error;
    }
});

Advanced: Custom Response Handling

By default, responses are parsed as JSON.

If the response is not JSON, you can override behavior:

await api.get<string>("/text-endpoint", {
    callbacks: {
        onSuccess: async (_, response) => {
            return await response.text();
        }
    }
});

License

MIT

About

A strongly-typed, middleware-driven HTTP client for TypeScript and JavaScript. Includes built-in retry strategies, automatic `Retry-After` handling, and flexible request middleware support.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages