Modern, fetch-compatible web request library with intelligent HTTP caching, retry strategies, and fault tolerance.
Modern, fetch-compatible web request library with intelligent HTTP caching, retry strategies, and advanced fault tolerance.
- 🌐 Fetch-Compatible API - Drop-in replacement for native fetch() with enhanced features
- 💾 Intelligent HTTP Caching - Respects Cache-Control, ETag, Last-Modified, and Expires headers (RFC 7234)
- 🔄 Multiple Cache Strategies - network-first, cache-first, stale-while-revalidate, network-only, cache-only
- 🔁 Advanced Retry System - Configurable retry with exponential/linear/constant backoff
- 🎯 Request/Response Interceptors - Middleware pattern for transforming requests and responses
- 🚫 Request Deduplication - Automatically deduplicate simultaneous identical requests
- 📘 TypeScript Generics - Type-safe response parsing with webrequest.getJson
- 🛡️ Better Fault Tolerance - Multi-endpoint fallback with retry strategies
- ⏱️ Timeout Support - Configurable request timeouts with AbortController
``bash`
pnpm install @push.rocks/webrequestor
npm install @push.rocks/webrequest
This package requires a modern JavaScript environment with ESM and TypeScript support.
`typescript
import { webrequest } from '@push.rocks/webrequest';
// Use exactly like fetch()
const response = await webrequest('https://api.example.com/data');
const data = await response.json();
// With options (fetch-compatible + enhanced)
const response = await webrequest('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' }),
timeout: 30000,
retry: true
});
`
`typescript
import { webrequest } from '@push.rocks/webrequest';
// GET JSON with type safety
interface User {
id: number;
name: string;
email: string;
}
const user = await webrequest.getJson
// user is typed as User
// POST JSON
const result = await webrequest.postJson('https://api.example.com/users', {
name: 'John Doe',
email: 'john@example.com'
});
// Other convenience methods
await webrequest.putJson(url, data);
await webrequest.patchJson(url, data);
await webrequest.deleteJson(url);
`
Always fetch from network, fall back to cache on failure. Respects HTTP caching headers.
`typescript`
const data = await webrequest.getJson('https://api.example.com/data', {
cacheStrategy: 'network-first'
});
Check cache first, only fetch from network if not cached or stale.
`typescript`
const data = await webrequest.getJson('https://api.example.com/data', {
cacheStrategy: 'cache-first',
cacheMaxAge: 60000 // 60 seconds
});
Return cached data immediately, update in background.
`typescript`
const data = await webrequest.getJson('https://api.example.com/data', {
cacheStrategy: 'stale-while-revalidate'
});
`typescript
// Always fetch from network, never cache
const data = await webrequest.getJson(url, {
cacheStrategy: 'network-only'
});
// Only use cache, never fetch from network
const data = await webrequest.getJson(url, {
cacheStrategy: 'cache-only'
});
`
The library automatically respects HTTP caching headers:
`typescript
// Server returns: Cache-Control: max-age=3600, ETag: "abc123"
const response = await webrequest('https://api.example.com/data', {
cacheStrategy: 'network-first'
});
// Subsequent requests automatically send:
// If-None-Match: "abc123"
// Server returns 304 Not Modified - cache is used
`
`typescriptsearch:${url.searchParams.get('q')}
const response = await webrequest('https://api.example.com/search?q=test', {
cacheStrategy: 'cache-first',
cacheKey: (request) => {
const url = new URL(request.url);
return ;`
}
});
`typescript`
const response = await webrequest('https://api.example.com/data', {
retry: true // Uses defaults: 3 attempts, exponential backoff
});
`typescriptRetry attempt ${attempt}, waiting ${nextDelay}ms
const response = await webrequest('https://api.example.com/data', {
retry: {
maxAttempts: 5,
backoff: 'exponential', // or 'linear', 'constant'
initialDelay: 1000, // 1 second
maxDelay: 30000, // 30 seconds
retryOn: [408, 429, 500, 502, 503, 504], // Status codes to retry
onRetry: (attempt, error, nextDelay) => {
console.log();`
}
}
});
`typescript`
const response = await webrequest('https://api1.example.com/data', {
fallbackUrls: [
'https://api2.example.com/data',
'https://api3.example.com/data'
],
retry: {
maxAttempts: 3,
backoff: 'exponential'
}
});
`typescript
import { webrequest } from '@push.rocks/webrequest';
// Add authentication to all requests
webrequest.addRequestInterceptor((request) => {
const headers = new Headers(request.headers);
headers.set('Authorization', Bearer ${getToken()});
return new Request(request, { headers });
});
// Log all responses
webrequest.addResponseInterceptor((response) => {
console.log(${response.status} ${response.url});
return response;
});
// Handle errors globally
webrequest.addErrorInterceptor((error) => {
console.error('Request failed:', error);
throw error;
});
`
`typescript`
const response = await webrequest('https://api.example.com/data', {
interceptors: {
request: [(req) => {
console.log('Sending:', req.url);
return req;
}],
response: [(res) => {
console.log('Received:', res.status);
return res;
}],
error: [(err) => {
console.error('Error:', err);
throw err;
}]
}
});
Automatically prevent duplicate simultaneous requests:
`typescript
// Only one actual network request is made
const [res1, res2, res3] = await Promise.all([
webrequest('https://api.example.com/data', { deduplicate: true }),
webrequest('https://api.example.com/data', { deduplicate: true }),
webrequest('https://api.example.com/data', { deduplicate: true }),
]);
// All three get the same response
`
For more control, use WebrequestClient to set default options:
`typescript
import { WebrequestClient } from '@push.rocks/webrequest';
const apiClient = new WebrequestClient({
logging: true,
timeout: 30000,
cacheStrategy: 'network-first',
retry: {
maxAttempts: 3,
backoff: 'exponential'
}
});
// Add global interceptors to this client
apiClient.addRequestInterceptor((request) => {
const headers = new Headers(request.headers);
headers.set('X-API-Key', process.env.API_KEY);
return new Request(request, { headers });
});
// All requests through this client use the configured defaults
const data = await apiClient.getJson('https://api.example.com/data');
// Standard fetch-compatible API also available
const response = await apiClient.request('https://api.example.com/data');
`
`typescript`
const response = await webrequest('https://api.example.com/data', {
timeout: 5000 // 5 seconds
});
`typescript`
const response = await webrequest('https://api.example.com/data', {
headers: {
'Authorization': 'Bearer token123',
'X-Custom-Header': 'value'
}
});
`typescript
// Clear all cached responses
await webrequest.clearCache();
// Clear specific cache entry
await webrequest.clearCache('https://api.example.com/data');
`
Full TypeScript support with generics for type-safe responses:
`typescript
interface ApiResponse
data: T;
status: 'success' | 'error';
message?: string;
}
interface User {
id: number;
name: string;
email: string;
}
// Fully typed response
const response = await webrequest.getJson
'https://api.example.com/user/1'
);
// response.data.id, response.data.name, response.data.email are all typed
`
`typescript`
webrequest(input: string | Request | URL, options?: IWebrequestOptions): Promise
`typescript`
webrequest.getJson
webrequest.postJson
webrequest.putJson
webrequest.patchJson
webrequest.deleteJson
`typescript`
webrequest.addRequestInterceptor(interceptor: TRequestInterceptor): void
webrequest.addResponseInterceptor(interceptor: TResponseInterceptor): void
webrequest.addErrorInterceptor(interceptor: TErrorInterceptor): void
webrequest.clearCache(url?: string): Promise
`typescript
interface IWebrequestOptions extends Omit
// Standard fetch options
method?: string;
headers?: HeadersInit;
body?: BodyInit;
// Enhanced options
cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached';
cacheStrategy?: 'network-first' | 'cache-first' | 'stale-while-revalidate' | 'network-only' | 'cache-only';
cacheMaxAge?: number; // milliseconds
cacheKey?: (request: Request) => string;
retry?: boolean | IRetryOptions;
fallbackUrls?: string[];
timeout?: number; // milliseconds
interceptors?: {
request?: TRequestInterceptor[];
response?: TResponseInterceptor[];
error?: TErrorInterceptor[];
};
deduplicate?: boolean;
logging?: boolean;
}
`
Version 4.0 is a complete rewrite of @push.rocks/webrequest with breaking changes. The v3 API has been completely removed - all v3 code must be migrated to v4.
- Fetch-Compatible API: Drop-in replacement for native fetch() with enhanced featuresCache-Control
- Intelligent HTTP Caching: Respects , ETag, Last-Modified, and Expires headers (RFC 7234)webrequest.getJson
- Multiple Cache Strategies: network-first, cache-first, stale-while-revalidate, network-only, cache-only
- Advanced Retry System: Configurable retry with exponential/linear/constant backoff
- Request/Response Interceptors: Middleware pattern for transforming requests and responses
- Request Deduplication: Automatically deduplicate simultaneous identical requests
- TypeScript Generics: Type-safe response parsing with
- Better Fault Tolerance: Enhanced multi-endpoint fallback with retry strategies
1. Removed WebRequest class - Use webrequest function or WebrequestClient class insteaduseCache
2. Cache API changed - Boolean replaced with explicit cacheStrategy optionsrequestMultiEndpoint()
3. Multi-endpoint API changed - replaced with fallbackUrls option
#### Basic Fetch-Compatible Usage
`typescript
// v3
import { WebRequest } from '@push.rocks/webrequest';
const client = new WebRequest();
const response = await client.request('https://api.example.com/data', {
method: 'GET'
});
const data = await response.json();
// v4 - Fetch-compatible
import { webrequest } from '@push.rocks/webrequest';
const response = await webrequest('https://api.example.com/data');
const data = await response.json();
`
#### JSON Convenience Methods
`typescript
// v3
const client = new WebRequest();
const data = await client.getJson('https://api.example.com/data', true);
// v4 - Function API
import { webrequest } from '@push.rocks/webrequest';
const data = await webrequest.getJson('https://api.example.com/data', {
cacheStrategy: 'cache-first'
});
// v4 - Client API (similar to v3)
import { WebrequestClient } from '@push.rocks/webrequest';
const client = new WebrequestClient({ logging: true });
const data = await client.getJson('https://api.example.com/data', {
cacheStrategy: 'cache-first'
});
`
#### Caching
`typescript
// v3 - Boolean flag
const data = await client.getJson(url, true); // useCache = true
// v4 - Explicit strategies
const data = await webrequest.getJson(url, {
cacheStrategy: 'cache-first',
cacheMaxAge: 60000 // 60 seconds
});
// v4 - HTTP header-based caching (automatic)
const data = await webrequest.getJson(url, {
cacheStrategy: 'network-first' // Respects Cache-Control headers
});
`
#### Multi-Endpoint Fallback
`typescript
// v3
const client = new WebRequest();
const response = await client.requestMultiEndpoint(
['https://api1.example.com/data', 'https://api2.example.com/data'],
{ method: 'GET' }
);
// v4
import { webrequest } from '@push.rocks/webrequest';
const response = await webrequest('https://api1.example.com/data', {
fallbackUrls: ['https://api2.example.com/data'],
retry: {
maxAttempts: 3,
backoff: 'exponential'
}
});
`
#### Timeout
`typescript
// v3
const response = await client.request(url, {
method: 'GET',
timeoutMs: 30000
});
// v4
const response = await webrequest(url, {
timeout: 30000 // milliseconds
});
`
`typescript
import { webrequest } from '@push.rocks/webrequest';
async function fetchUserData(userId: string) {
interface User {
id: string;
name: string;
email: string;
}
const user = await webrequest.getJson
https://api.example.com/users/${userId},
{
// Caching
cacheStrategy: 'stale-while-revalidate',
cacheMaxAge: 300000, // 5 minutes
// Retry
retry: {
maxAttempts: 3,
backoff: 'exponential',
retryOn: [500, 502, 503, 504]
},
// Fallback
fallbackUrls: [
'https://api-backup.example.com/users/${userId}'
],
// Timeout
timeout: 10000, // 10 seconds
// Deduplication
deduplicate: true,
// Per-request interceptor
interceptors: {
request: [(req) => {
console.log(Fetching user ${userId});
return req;
}]
}
}
);
return user;
}
`
`typescript
import { WebrequestClient } from '@push.rocks/webrequest';
class ApiClient {
private client: WebrequestClient;
constructor(private baseUrl: string, private apiKey: string) {
this.client = new WebrequestClient({
timeout: 30000,
cacheStrategy: 'network-first',
retry: {
maxAttempts: 3,
backoff: 'exponential'
}
});
// Add auth interceptor
this.client.addRequestInterceptor((request) => {
const headers = new Headers(request.headers);
headers.set('Authorization', Bearer ${this.apiKey});
headers.set('Content-Type', 'application/json');
return new Request(request, { headers });
});
}
async getUser(id: string): Promise
return this.client.getJson);
}
async createUser(data: CreateUserData): Promise
return this.client.postJson, data);
}
async updateUser(id: string, data: UpdateUserData): Promise
return this.client.putJson, data);
}
async deleteUser(id: string): Promise
await this.client.deleteJson(${this.baseUrl}/users/${id});
}
}
// Usage
const api = new ApiClient('https://api.example.com', process.env.API_KEY);
const user = await api.getUser('123');
``
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.