@blimu/fetch
Universal HTTP fetch client with hooks, retries, and streaming support for browser and Node.js.
Features
- 🌐 Universal: Works in both browser and Node.js environments
- 🔄 Smart Retries: Configurable retry strategies (exponential, linear, custom)
- 🎣 Hooks System: Extensible lifecycle hooks for request/response handling
- 📡 Streaming Support: SSE, NDJSON, and chunked streaming parsers
- 🚨 Type-Safe Errors: Specific error classes for different HTTP status codes
- 🔒 Authentication: Built-in support for Bearer tokens, Basic auth, and API keys
- ⚡ Zero Dependencies: Uses native fetch API
Installation
npm install @blimu/fetch
yarn add @blimu/fetch
pnpm add @blimu/fetch
Quick Start
import { FetchClient } from "@blimu/fetch";
const client = new FetchClient({
baseURL: "https://api.example.com",
headers: {
"Content-Type": "application/json",
},
});
const data = await client.request({
path: "/users",
method: "GET",
});
Configuration
Basic Configuration
const client = new FetchClient({
baseURL: "https://api.example.com",
headers: {
"X-Custom-Header": "value",
},
timeoutMs: 5000,
credentials: "include",
});
Authentication
Authentication is configured using the auth option with strategies:
const client = new FetchClient({
baseURL: "https://api.example.com",
auth: {
strategies: [
{
type: "bearer",
token: "your-token-here",
},
],
},
});
const client = new FetchClient({
baseURL: "https://api.example.com",
auth: {
strategies: [
{
type: "bearer",
token: async () => {
return await getToken();
},
headerName: "Authorization",
},
],
},
});
const client = new FetchClient({
baseURL: "https://api.example.com",
auth: {
strategies: [
{
type: "basic",
username: "user",
password: "pass",
},
],
},
});
const client = new FetchClient({
baseURL: "https://api.example.com",
auth: {
strategies: [
{
type: "apiKey",
key: "your-api-key",
location: "header",
name: "X-API-Key",
},
],
},
});
const client = new FetchClient({
baseURL: "https://api.example.com",
auth: {
strategies: [
{
type: "apiKey",
key: "your-api-key",
location: "query",
name: "api_key",
},
],
},
});
const client = new FetchClient({
baseURL: "https://api.example.com",
auth: {
strategies: [
{
type: "bearer",
token: "token",
},
{
type: "apiKey",
key: "api-key",
location: "header",
name: "X-API-Key",
},
],
},
});
const client = new FetchClient({
baseURL: "https://api.example.com",
auth: {
strategies: [
{
type: "custom",
apply: async (headers, url) => {
const token = await getCustomToken();
headers.set("X-Custom-Auth", token);
},
},
],
},
});
Hooks System
The hooks system allows you to intercept and modify requests at different lifecycle stages.
Available Hooks
beforeRequest: Before the request is made (can modify request)
afterRequest: After response is received, before parsing
afterResponse: After response is parsed
onError: When an error occurs
beforeRetry: Before a retry attempt
afterRetry: After a retry attempt
onTimeout: When a timeout occurs
onStreamStart: When streaming starts
onStreamChunk: For each stream chunk (can transform)
onStreamEnd: When streaming ends
Using Hooks
const client = new FetchClient({
baseURL: "https://api.example.com",
hooks: {
beforeRequest: [
(ctx) => {
ctx.init.headers.set("X-Request-ID", generateId());
},
async (ctx) => {
const token = await refreshToken();
ctx.init.headers.set("Authorization", `Bearer ${token}`);
},
],
afterResponse: [
(ctx) => {
console.log("Response:", ctx.data);
},
],
onError: [
(ctx) => {
console.error("Request failed:", ctx.error);
},
],
},
});
Dynamic Hook Registration
const client = new FetchClient({ baseURL: "https://api.example.com" });
client.useHook("beforeRequest", (ctx) => {
console.log("Making request to:", ctx.url);
});
const hook = (ctx) => console.log(ctx);
client.useHook("beforeRequest", hook);
client.removeHook("beforeRequest", hook);
client.clearHooks("beforeRequest");
client.clearHooks();
Retry Configuration
Exponential Backoff (Default)
const client = new FetchClient({
baseURL: "https://api.example.com",
retry: {
retries: 3,
strategy: "exponential",
backoffMs: 100,
retryOn: [429, 500, 502, 503, 504],
},
});
Linear Backoff
const client = new FetchClient({
baseURL: "https://api.example.com",
retry: {
retries: 3,
strategy: "linear",
backoffMs: 200,
retryOn: [500, 502, 503],
},
});
Custom Retry Strategy
const client = new FetchClient({
baseURL: "https://api.example.com",
retry: {
retries: 3,
strategy: (attempt, baseBackoff) => {
return baseBackoff * (attempt + 1) * 2;
},
retryOn: [500],
retryOnError: (error) => {
return error instanceof NetworkError;
},
},
});
Streaming
Server-Sent Events (SSE)
for await (const chunk of client.requestStream({
path: "/events",
method: "GET",
contentType: "text/event-stream",
streamingFormat: "sse",
})) {
console.log("Event:", chunk);
}
NDJSON (Newline-Delimited JSON)
for await (const item of client.requestStream({
path: "/items",
method: "GET",
contentType: "application/x-ndjson",
streamingFormat: "ndjson",
})) {
console.log("Item:", item);
}
Chunked Streaming
for await (const chunk of client.requestStream({
path: "/stream",
method: "GET",
contentType: "application/octet-stream",
streamingFormat: "chunked",
})) {
console.log("Chunk:", chunk);
}
Error Handling
The package provides specific error classes for different HTTP status codes, enabling instanceof checks in catch blocks.
Error Classes
4xx Client Errors:
BadRequestError (400)
UnauthorizedError (401)
ForbiddenError (403)
NotFoundError (404)
MethodNotAllowedError (405)
ConflictError (409)
UnprocessableEntityError (422)
TooManyRequestsError (429)
ClientError (generic 4xx)
5xx Server Errors:
InternalServerError (500)
BadGatewayError (502)
ServiceUnavailableError (503)
GatewayTimeoutError (504)
ServerError (generic 5xx)
Using Error Classes
import {
FetchClient,
NotFoundError,
UnauthorizedError,
ServerError,
} from "@blimu/fetch";
try {
await client.request({ path: "/users/123", method: "GET" });
} catch (error) {
if (error instanceof NotFoundError) {
console.log("User not found");
} else if (error instanceof UnauthorizedError) {
console.log("Unauthorized - refresh token");
} else if (error instanceof ServerError) {
console.log("Server error:", error.status);
} else {
console.error("Unexpected error:", error);
}
}
Browser vs Node.js
Browser
Works out of the box in modern browsers (Chrome 42+, Firefox 39+, Safari 10.1+, Edge 14+):
import { FetchClient } from "@blimu/fetch";
const client = new FetchClient({
baseURL: "https://api.example.com",
});
Node.js
Node.js 22+: Native fetch is available, works out of the box.
Node.js < 22: Provide a custom fetch implementation:
import { FetchClient } from "@blimu/fetch";
import { fetch } from "undici";
const client = new FetchClient({
baseURL: "https://api.example.com",
fetch,
});
API Reference
FetchClient
Constructor
new FetchClient(config?: FetchClientConfig)
Methods
request<T>(options: RequestOptions): Promise<T> - Make an HTTP request
requestStream<T>(options: StreamingRequestOptions): AsyncGenerator<T> - Make a streaming request
addAuthStrategy(strategy: AuthStrategy): void - Add an authentication strategy
clearAuthStrategies(): void - Remove all authentication strategies
useHook(stage: string, hook: Hook): void - Register a hook
removeHook(stage: string, hook: Hook): boolean - Remove a hook
clearHooks(stage?: string): void - Clear hooks
Types
interface FetchClientConfig {
baseURL?: string;
headers?: Record<string, string>;
timeoutMs?: number;
retry?: RetryConfig;
hooks?: HooksConfig;
auth?: AuthConfig;
fetch?: typeof fetch;
credentials?: RequestCredentials;
}
interface AuthConfig {
strategies: AuthStrategy[];
}
type AuthStrategy =
| BearerAuthStrategy
| BasicAuthStrategy
| ApiKeyAuthStrategy
| CustomAuthStrategy;
interface RequestOptions extends RequestInit {
path: string;
method: string;
query?: Record<string, any>;
}
interface StreamingRequestOptions extends RequestOptions {
contentType: string;
streamingFormat?: "sse" | "ndjson" | "chunked";
}
License
MIT