@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 authStrategies option:
const client = new FetchClient({
baseURL: 'https://api.example.com',
authStrategies: [
{
type: 'bearer',
token: 'your-token-here',
},
],
});
const client = new FetchClient({
baseURL: 'https://api.example.com',
authStrategies: [
{
type: 'bearer',
token: async () => {
return await getToken();
},
headerName: 'Authorization',
},
],
});
const client = new FetchClient({
baseURL: 'https://api.example.com',
authStrategies: [
{
type: 'basic',
username: 'user',
password: 'pass',
},
],
});
const client = new FetchClient({
baseURL: 'https://api.example.com',
authStrategies: [
{
type: 'apiKey',
key: 'your-api-key',
location: 'header',
name: 'X-API-Key',
},
],
});
const client = new FetchClient({
baseURL: 'https://api.example.com',
authStrategies: [
{
type: 'apiKey',
key: 'your-api-key',
location: 'query',
name: 'api_key',
},
],
});
const client = new FetchClient({
baseURL: 'https://api.example.com',
authStrategies: [
{
type: 'bearer',
token: 'token',
},
{
type: 'apiKey',
key: 'api-key',
location: 'header',
name: 'X-API-Key',
},
],
});
const client = new FetchClient({
baseURL: 'https://api.example.com',
authStrategies: [
{
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;
authStrategies?: AuthStrategy[];
fetch?: typeof fetch;
credentials?: RequestCredentials;
}
type AuthStrategy =
| BearerAuthStrategy
| BasicAuthStrategy
| ApiKeyAuthStrategy
| CustomAuthStrategy;
interface RequestOptions extends RequestInit {
path: string;
method: string;
query?: Record<string, any> | undefined;
}
interface StreamingRequestOptions extends RequestOptions {
contentType: string;
streamingFormat?: 'sse' | 'ndjson' | 'chunked';
}
License
MIT