@pingpong-js/fetch
v1.0.2
Published
Universal HTTP client for Node.js and browsers with automatic environment detection
Maintainers
Readme
🏓 @pingpong-js/fetch
Universal HTTP client for Node.js and browsers with automatic environment detection
A lightweight, universal HTTP client that automatically detects your environment and uses the optimal implementation:
- Node.js: Uses
undicifor high performance - Browser: Uses native
fetch()API
✨ Features
- 🌐 Universal - Works seamlessly in Node.js and browsers
- ⚡ Fast - Uses
undiciin Node.js, nativefetchin browsers - 🔄 Zero Config - Automatic environment detection
- 📦 Small - Minimal dependencies (~5-6KB gzipped), tree-shakeable, optimized
- 🎯 Type-Safe - Full TypeScript support with generics (
get<T>,post<T>, etc.) - 📊 Axios-style
response.data- Auto-parsed JSON data, no more.then(r => r.json())(v1.4.0) - 🎣 Event Listeners -
onRequest,onResponse,onError,onRetry,onAborthooks (v1.4.0) - 🔧 Query Parameters - Built-in query param support at client and request level (v1.2.0)
- 📋 Case-Insensitive Headers - RFC 2616 compliant header access (v1.2.0)- 🍪 Credentials Control - Control cookie sending with
credentialsoption (Browser)- 🔁 Retry Logic - Built-in retry with exponential backoff - 📄 Response Methods - Chainable methods:
.json(),.text(),.raw(),.ok(),.isError(),.header() - 📡 Streaming - Support for streaming large files and responses
- 🛠️ Convenience Methods -
.get(),.post(),.put(),.delete(),.patch(),.head(),.options() - ⏱️ Timeout Support - Configurable timeouts at client and request level
- 🔀 Proxy Support - HTTP proxy support (Node.js)
- 🚫 Request Cancellation - AbortSignal support for cancelling requests
- 🔁 Redirect Handling - Automatic redirect following with configurable limits
📦 Installation
npm install @pingpong-js/fetch🔌 Optional Plugins (Lodash-style)
Install only the plugins you need:
# Smart caching (memory, localStorage, IndexedDB)
npm install @pingpong-js/plugin-cache
# Advanced retry, circuit breaker, rate limiting
npm install @pingpong-js/plugin-retry
# Zod schema validation for type-safe responses
npm install @pingpong-js/plugin-zodimport pingpong from '@pingpong-js/fetch';
import { createCache } from '@pingpong-js/plugin-cache';
import { createCircuitBreaker } from '@pingpong-js/plugin-retry';
import { withZod } from '@pingpong-js/plugin-zod';🚀 Quick Start
import pingpong from '@pingpong-js/fetch';
// Simple GET request - JSON is auto-parsed! (Axios-style)
const response = await pingpong.get('https://api.mockly.codes/users/1');
console.log(response.data); // Already parsed JSON!
console.log(response.data.name); // Access properties directly
// Type-safe with generics
interface User {
id: number;
name: string;
email: string;
}
const userRes = await pingpong.get<User>('https://api.mockly.codes/users/1');
console.log(userRes.data.name); // TypeScript knows this is string!
// POST with JSON (automatically stringified, response auto-parsed)
const newUser = await pingpong.post<User>('https://api.mockly.codes/users', {
name: 'John Doe',
email: '[email protected]'
});
console.log(newUser.data.id); // New user's ID
// For non-JSON responses, use json: false
const textRes = await pingpong.get('https://example.com/robots.txt', { json: false });
console.log(textRes.body); // Raw text response
console.log(textRes.data); // null (not parsed)
// Legacy .json() method still works
const user = await pingpong.get('https://api.mockly.codes/users/1').then(r => r.json());Custom Configuration
import pingpong from '@pingpong-js/fetch';
// Create a custom client from pingpong
const api = pingpong.create({
baseURL: 'https://api.mockly.codes',
timeout: 5000,
params: { api_key: 'secret' } // Default params for all requests
});
// Use with query parameters
const response = await api.get('/users', {
params: { page: 1, limit: 10 } // Merged with client params
});
// Or import HttpClient directly for full control
import { HttpClient } from '@pingpong-js/fetch';
const customClient = new HttpClient({ baseURL: 'https://other-api.com' });📊 Performance
- Bundle Size: ~5-6KB gzipped (25% smaller than axios)
- Throughput: ~4,250 req/s average
- Latency: ~0.24ms average response time
- Build Time: Optimized with stripped source maps and comments
📚 Table of Contents
📖 Examples
Check out the examples directory for complete working examples of all features:
Node.js Examples
- Basic GET - Simple GET request
- POST with JSON - Send JSON data
- Headers & Auth - Authentication and custom headers
- Response Methods - .json(), .text(), .ok(), etc.
- Convenience Methods - .get(), .post(), .put(), .delete()
- FormData Upload - File uploads with FormData
- Error Handling - Handle errors gracefully
- Retry Logic - Automatic retry with backoff
- Timeout - Configure timeouts
- Abort Signal - Cancel requests
- Proxy Support - Route through proxy
- Credentials - Cookie control (Browser)
Browser Examples
- Browser Usage - Interactive browser example
- Environment Detection - Automatic detection
See examples/README.md for complete example documentation.
📖 API Reference
pingpong
The default pre-configured HTTP client instance, ready to use out of the box.
import pingpong from '@pingpong-js/fetch';
// Use directly
const response = await pingpong.get('https://api.example.com/users');
// Create a custom client with options
const api = pingpong.create({
baseURL: 'https://api.example.com',
timeout: 5000
});HttpClient
For full control, you can instantiate HttpClient directly:
import { HttpClient } from '@pingpong-js/fetch';
const client = new HttpClient(options?: HttpClientOptions);Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| baseURL | string | undefined | Base URL prepended to relative URLs |
| params | Record<string, any> | undefined | Default query parameters for all requests (v1.2.0) |
| timeout | number | undefined | Default request timeout in milliseconds |
| headers | HttpHeaders | {} | Default headers for all requests |
| proxy | string | undefined | HTTP proxy URL (Node.js only) |
| followRedirects | boolean | true | Automatically follow redirects |
| maxRedirects | number | 10 | Maximum number of redirects to follow |
| credentials | 'omit' \| 'same-origin' \| 'include' | undefined | Controls cookie sending behavior (Browser only) |
| retries | number | 0 | Default number of retry attempts |
| retryDelay | number | 1000 | Default delay between retries (ms, uses exponential backoff) |
| retryCondition | (response: HttpResponse) => boolean | See below | Function to determine if request should be retried |
| json | boolean | true | Auto-parse JSON responses and populate data property (v1.4.0) |
Default Retry Condition:
(response) => response.status === 0 || response.status >= 500 || response.status === 429Retries on network errors (status 0), server errors (5xx), and rate limiting (429).
Methods
Core Methods
send(request: HttpRequest, options?: RequestOptions): Promise<HttpResponse>
Send a custom HTTP request.
const response = await pingpong.send({
method: 'GET',
url: 'https://api.mockly.codes/data',
headers: { 'Authorization': 'Bearer token' }
});sendStream(request: HttpRequest, options?: RequestOptions): Promise<StreamResponse>
Send a request and return a stream instead of buffering the response.
const { stream, status, headers } = await pingpong.sendStream({
method: 'GET',
url: 'https://api.mockly.codes/large-file'
});HTTP Methods
All HTTP methods accept an optional RequestOptions parameter to override client defaults.
// GET request returning response with chainable methods
pingpong.get(url: string, options?: RequestOptions): Promise<HttpResponse>
// GET returning stream
pingpong.getStream(url: string, options?: RequestOptions): Promise<StreamResponse>
// POST request (data automatically stringified if object)
pingpong.post(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>
// PUT request
pingpong.put(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>
// PATCH request
pingpong.patch(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>
// DELETE request
pingpong.delete(url: string, options?: RequestOptions): Promise<HttpResponse>
// HEAD request
pingpong.head(url: string, options?: RequestOptions): Promise<HttpResponse>
// OPTIONS request
pingpong.options(url: string, options?: RequestOptions): Promise<HttpResponse>Event Listeners
Register listeners for various request lifecycle events:
// Called before each request (can modify request)
pingpong.onRequest(listener: (req: HttpRequest) => HttpRequest | void): () => void
// Called after each response (can modify response)
pingpong.onResponse(listener: (res: HttpResponse) => HttpResponse | void): () => void
// Called when a request fails
pingpong.onError(listener: (error: Error, req: HttpRequest) => void): () => void
// Called before each retry attempt
pingpong.onRetry(listener: (attempt: number, req: HttpRequest, res: HttpResponse) => void): () => void
// Called when a request is aborted
pingpong.onAbort(listener: (req: HttpRequest) => void): () => void
// Remove listeners
pingpong.off(event?: string): void // Clear specific or all listeners
// Get listener count
pingpong.listenerCount(event: string): numberFactory Method
create(options: HttpClientOptions): HttpClient
Create a new client instance with custom options.
const api = pingpong.create({
baseURL: 'https://api.mockly.codes',
timeout: 5000
});Response Methods
All HTTP methods return an HttpResponse with these helper methods:
ok(): boolean
Check if status is 2xx (200-299).
const response = await pingpong.get('/data');
if (response.ok()) {
console.log('Success!');
}isError(): boolean
Check if status is 4xx or 5xx.
const response = await pingpong.get('/data');
if (response.isError()) {
console.error('Request failed:', response.status);
}redirected(): boolean
Check if response was a redirect (3xx).
const response = await pingpong.get('/redirect');
console.log(response.redirected()); // truejson<T>(): T
Parse response body as JSON.
const user = await pingpong.get('https://api.mockly.codes/users/1').then(r => r.json());text(): string
Get response body as text.
const html = await pingpong.get('https://example.com').then(r => r.text());raw(): HttpResponseBase
Get the raw response object (without methods).
const response = await pingpong.get('/data');
const raw = response.raw();header(name: string): string | undefined
Get response header value (case-insensitive, RFC 2616 compliant).
const response = await pingpong.get('/data');
// All of these return the same value
response.header('content-type'); // 'application/json'
response.header('Content-Type'); // 'application/json'
response.header('CONTENT-TYPE'); // 'application/json'Types
HttpRequest
interface HttpRequest {
method: HttpMethod; // 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
url: string; // Request URL
headers?: HttpHeaders; // Request headers
body?: string | Uint8Array | Blob | FormData; // Request body
timeout?: number; // Request timeout (ms)
signal?: AbortSignal; // Abort signal for cancellation
proxy?: string; // Proxy URL (Node.js only)
followRedirects?: boolean; // Follow redirects
maxRedirects?: number; // Max redirects to follow
credentials?: 'omit' | 'same-origin' | 'include'; // Cookie behavior (Browser)
retries?: number; // Retry attempts
retryDelay?: number; // Retry delay (ms)
validateStatus?: (status: number) => boolean; // Status validation
}HttpResponse
interface HttpResponse<T = any> {
status: number; // HTTP status code
statusText: string; // HTTP status text
headers: HttpHeaders; // Response headers
body: string; // Response body (as string)
data: T; // Auto-parsed JSON data (v1.4.0)
time: number; // Request duration (ms)
size: number; // Response size (bytes)
// Chainable methods
raw(): HttpResponseBase; // Get raw response
json<R = T>(): R; // Parse as JSON (legacy)
text(): string; // Get body as text
header(name: string): string | undefined; // Get single header (case-insensitive)
ok(): boolean; // Check if 2xx
redirected(): boolean; // Check if 3xx
isError(): boolean; // Check if 4xx/5xx
}StreamResponse
interface StreamResponse {
status: number; // HTTP status code
statusText: string; // HTTP status text
headers: HttpHeaders; // Response headers
stream: ReadableStream<Uint8Array> | NodeJS.ReadableStream; // Response stream
time: number; // Request duration (ms)
}RequestOptions
All options from HttpClientOptions can be overridden per-request via RequestOptions:
interface RequestOptions {
params?: Record<string, any>; // Query parameters (merged with client params)
timeout?: number;
headers?: HttpHeaders;
signal?: AbortSignal;
proxy?: string; // Node.js only
followRedirects?: boolean;
maxRedirects?: number;
credentials?: 'omit' | 'same-origin' | 'include'; // Cookie behavior (Browser)
retries?: number;
retryDelay?: number;
retryCondition?: (response: HttpResponse) => boolean;
json?: boolean; // Auto-parse JSON (default: true, v1.4.0)
}💡 Examples
Basic Usage
import pingpong from '@pingpong-js/fetch';
// GET request with chainable response methods
const response = await pingpong.get('https://api.mockly.codes/users');
console.log(response.status); // 200
console.log(response.text()); // Response body as string
console.log(response.time); // Request duration in ms
console.log(response.size); // Response size in bytes
console.log(response.ok()); // true (status 2xx)
console.log(response.getHeaders()); // Response headersJSON Parsing
import pingpong from '@pingpong-js/fetch';
interface User {
id: number;
name: string;
email: string;
}
// NEW in v1.4.0: Auto-parsed JSON in response.data (Axios-style)
const response = await pingpong.get<User>('https://api.mockly.codes/users/1');
console.log(response.data.name); // Already parsed! TypeScript knows it's User
// POST with auto-parsed response
const newUser = await pingpong.post<User>('https://api.mockly.codes/users', {
name: 'John Doe',
email: '[email protected]'
});
console.log(newUser.data.id); // Response is auto-parsed
// For non-JSON responses, disable auto-parsing
const htmlResponse = await pingpong.get('https://example.com', { json: false });
console.log(htmlResponse.body); // Raw HTML
console.log(htmlResponse.data); // null (not parsed)
// Legacy .json() method still works
const user = await pingpong.get('https://api.mockly.codes/users/1').then(r => r.json<User>());
console.log(user.name); // Works the same as beforeRetry Logic
import pingpong from '@pingpong-js/fetch';
// Configure retry at client level using pingpong.create()
const client = pingpong.create({
retries: 3, // Retry up to 3 times
retryDelay: 1000, // Initial delay: 1 second (exponential backoff)
retryCondition: (response) => {
// Retry on server errors or rate limits
return response.status >= 500 || response.status === 429;
}
});
// All requests will automatically retry
const response = await client.get('https://api.mockly.codes/flaky-endpoint');
// Override retry for specific request
const response2 = await client.get('https://api.mockly.codes/data', {
retries: 5, // Retry 5 times for this request
retryDelay: 2000, // 2 second initial delay
retryCondition: (res) => res.status === 503 // Only retry on 503
});
// Disable retry for specific request
const response3 = await client.get('https://api.mockly.codes/data', {
retries: 0 // No retries
});Retry Behavior:
- Uses exponential backoff: delay × 2^attempt (1s, 2s, 4s, 8s...)
- Respects abort signals (won't retry if cancelled)
- Default condition: retries on status 0 (network error), 5xx (server error), or 429 (rate limit)
Streaming
import pingpong from '@pingpong-js/fetch';
// Stream large file download (Node.js)
const streamResponse = await pingpong.getStream('https://example.com/large-file.zip');
// Node.js: ReadableStream
const stream = streamResponse.stream as NodeJS.ReadableStream;
const fs = require('fs');
const writeStream = fs.createWriteStream('downloaded-file.zip');
stream.pipe(writeStream);
// Browser: ReadableStream<Uint8Array>
const streamResponse2 = await pingpong.getStream('https://example.com/large-file.zip');
const reader = streamResponse2.stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Process chunk
console.log('Received chunk:', value.length, 'bytes');
// Save to file, process data, etc.
}
// Stream POST request
const streamResponse3 = await pingpong.sendStream({
method: 'POST',
url: 'https://api.mockly.codes/upload',
body: fileData
});Request Cancellation
import pingpong from '@pingpong-js/fetch';
const controller = new AbortController();
// Start request with abort signal
const requestPromise = pingpong.get('https://api.mockly.codes/slow-endpoint', {
signal: controller.signal
});
// Cancel the request
controller.abort();
// Handle cancellation
try {
const response = await requestPromise;
} catch (error) {
// Request was cancelled
console.log('Request cancelled');
}
// React component example
import { useEffect, useState } from 'react';
function DataComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const response = await pingpong.get('https://api.mockly.codes/data', {
signal: controller.signal
});
setData(response.json());
} catch (error) {
if (!controller.signal.aborted) {
console.error('Failed to fetch:', error);
}
}
}
fetchData();
// Cancel request on unmount
return () => controller.abort();
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}Query Parameters
import pingpong from '@pingpong-js/fetch';
// Client-level default params using pingpong.create()
const client = pingpong.create({
baseURL: 'https://api.example.com',
params: { api_key: 'secret', version: 'v1' } // Applied to all requests
});
// Request without params uses client defaults
const response1 = await client.get('/users');
// → https://api.example.com/users?api_key=secret&version=v1
// Request-level params are merged with client params
const response2 = await client.get('/users', {
params: { page: 1, limit: 10 } // Added to client params
});
// → https://api.example.com/users?api_key=secret&version=v1&page=1&limit=10
// Request params override client params
const response3 = await client.get('/users', {
params: { version: 'v2', page: 2 } // version=v2 overrides client's v1
});
// → https://api.example.com/users?api_key=secret&version=v2&page=2
// Array parameters (automatically repeated)
const response4 = await client.get('/products', {
params: {
tags: ['new', 'featured', 'bestseller'],
colors: ['red', 'blue']
}
});
// → /products?tags=new&tags=featured&tags=bestseller&colors=red&colors=blue
// Simple request params without custom client
const response5 = await pingpong.get('https://api.example.com/users', {
params: { search: 'john', active: true }
});
// → https://api.example.com/users?search=john&active=trueBenefits:
- No manual URL construction
- Automatic encoding and escaping
- Clean separation of URL and params
- Request-level params override client defaults
- Array support built-in
Event Listeners (Interceptors)
import pingpong from '@pingpong-js/fetch';
// onRequest - Called before each request (can modify request)
pingpong.onRequest(req => {
req.headers = { ...req.headers, 'Authorization': `Bearer ${getToken()}` };
return req; // Return modified request
});
// onResponse - Called after each response (can modify response)
pingpong.onResponse(res => {
console.log(`${res.status} - ${res.time}ms`);
return res;
});
// onError - Called when a request fails
pingpong.onError((error, request) => {
console.error('Request failed:', request.url, error.message);
reportToSentry(error);
});
// onRetry - Called before each retry attempt
pingpong.onRetry((attempt, request, response) => {
console.log(`Retry ${attempt} for ${request.url} (status: ${response.status})`);
});
// onAbort - Called when a request is aborted
pingpong.onAbort(request => {
console.log('Request aborted:', request.url);
});Multiple Listeners per Event:
// Add multiple listeners - all will be called in order
const unsub1 = pingpong.onRequest(req => {
console.log('Logger:', req.method, req.url);
});
const unsub2 = pingpong.onRequest(req => {
req.headers = { ...req.headers, 'X-Request-ID': uuid() };
return req;
});
const unsub3 = pingpong.onRequest(req => {
req.headers = { ...req.headers, 'Authorization': `Bearer ${token}` };
return req;
});
// Check listener count
console.log(pingpong.listenerCount('request')); // 3
// Unsubscribe individual listeners
unsub1(); // Remove logger
console.log(pingpong.listenerCount('request')); // 2
// Clear all listeners of a type
pingpong.off('request');
// Clear ALL listeners
pingpong.off();Real-World Example: Auth + Logging + Error Handling:
import pingpong from '@pingpong-js/fetch';
const api = pingpong.create({ baseURL: 'https://api.myapp.com' });
// 1. Add auth token
api.onRequest(req => {
const token = localStorage.getItem('token');
if (token) {
req.headers = { ...req.headers, 'Authorization': `Bearer ${token}` };
}
return req;
});
// 2. Log all requests
api.onRequest(req => {
console.log(`→ ${req.method} ${req.url}`);
});
// 3. Log all responses
api.onResponse(res => {
console.log(`← ${res.status} (${res.time}ms)`);
return res;
});
// 4. Handle 401 errors globally
api.onResponse(async res => {
if (res.status === 401) {
console.log('Token expired, redirecting to login...');
window.location.href = '/login';
}
return res;
});
// 5. Report errors
api.onError((error, req) => {
analytics.track('api_error', { url: req.url, error: error.message });
});
// Now all requests go through these listeners
const users = await api.get('/users');Benefits:
- Multiple listeners per event type
- Listeners can modify request/response
- Easy unsubscribe with returned function
- Clear all with
off() - Per-client listeners (don't affect other clients)
Built-in Plugins
Ready-to-use event listeners for common use cases. Import and plug in!
import pingpong, {
withAuth,
withApiKey,
withBasicAuth,
withRequestId,
withHeaders,
withTimeout,
withRequestLogging,
withResponseLogging,
withRetryLogging,
withErrorReporting,
withValidation,
withSlowRequestWarning,
withStatusHandlers,
withThrowOnStatus,
} from '@pingpong-js/fetch';Authentication Plugins
// Bearer token auth
pingpong.onRequest(withAuth(() => localStorage.getItem('token')));
pingpong.onRequest(withAuth(async () => await getTokenFromStore())); // Async supported
// API key auth
pingpong.onRequest(withApiKey('my-api-key'));
pingpong.onRequest(withApiKey(() => process.env.API_KEY, 'X-Custom-Header'));
// Basic auth
pingpong.onRequest(withBasicAuth('username', 'password'));Request Enhancement Plugins
// Add unique request ID for tracing
pingpong.onRequest(withRequestId()); // Adds X-Request-ID header
pingpong.onRequest(withRequestId('X-Correlation-ID', () => crypto.randomUUID()));
// Add custom headers to all requests
pingpong.onRequest(withHeaders({ 'X-App-Version': '1.0.0' }));
pingpong.onRequest(withHeaders(() => ({ 'X-Timestamp': Date.now().toString() })));
// Default timeout for all requests (only if not already set)
pingpong.onRequest(withTimeout(10000)); // 10 secondsLogging Plugins
// Log requests
pingpong.onRequest(withRequestLogging());
pingpong.onRequest(withRequestLogging({ prefix: '🚀', headers: true }));
// Log responses
pingpong.onResponse(withResponseLogging());
pingpong.onResponse(withResponseLogging({ prefix: '📥', body: true }));
// Combined logging (returns [requestListener, responseListener])
const [reqLog, resLog] = withLogging({ prefix: '[API]' });
pingpong.onRequest(reqLog);
pingpong.onResponse(resLog);
// Log retry attempts
pingpong.onRetry(withRetryLogging({ prefix: '🔄' }));Response Processing Plugins
// Custom validation
pingpong.onResponse(withValidation(data => {
if (!data.id) throw new Error('Missing id in response');
}));
// Warn on slow requests
pingpong.onResponse(withSlowRequestWarning(3000, (res) => {
console.warn(`Slow request: ${res.time}ms`);
}));
// Handle specific status codes
pingpong.onResponse(withStatusHandlers({
401: () => { window.location.href = '/login'; },
403: () => { showForbiddenMessage(); },
429: (res) => { console.log('Rate limited!'); },
}));
// Throw on specific status codes
pingpong.onResponse(withThrowOnStatus([401, 403, 500]));Error Handling Plugins
// Report errors to external service
pingpong.onError(withErrorReporting((error, req) => {
Sentry.captureException(error, { extra: { url: req.url } });
}));Case-Insensitive Headers
import pingpong from '@pingpong-js/fetch';
const response = await pingpong.get('https://api.example.com/data');
// Access headers with any casing (RFC 2616 compliant)
response.header('content-type'); // 'application/json'
response.header('Content-Type'); // 'application/json'
response.header('CONTENT-TYPE'); // 'application/json'
response.header('CoNtEnT-tYpE'); // 'application/json'
// All return the same value
const contentType = response.header('content-type');
if (contentType?.includes('json')) {
const data = response.data; // Already auto-parsed!
}Benefits:
- RFC 2616 HTTP spec compliant
- No case-sensitivity bugs
- Works with any casing variation
- Clean, predictable API
Credentials and Cookie Control (Browser)
import pingpong from '@pingpong-js/fetch';
// Control cookie sending behavior using pingpong.create()
const client = pingpong.create({
credentials: 'include' // Send cookies with all requests, even cross-origin
});
// Make authenticated requests with cookies
const response = await client.get('https://api.example.com/user/profile');
// Override per-request
const publicData = await client.get('https://api.example.com/public', {
credentials: 'omit' // Don't send cookies for this request
});
// Same-origin only (default browser behavior)
const sameOrigin = await client.get('https://api.example.com/data', {
credentials: 'same-origin' // Only send cookies for same-origin requests
});Credentials Options:
'omit'- Never send cookies'same-origin'- Send cookies only for same-origin requests (browser default)'include'- Always send cookies, even for CORS/cross-origin requests
Benefits:
- Control cookie behavior explicitly
- Follows Fetch API standard
- Works seamlessly with authentication flows
- Per-request override capability
Note: This option only works in browsers. Node.js requests should manage cookies manually using headers.
Base URL and Headers
import pingpong from '@pingpong-js/fetch';
// Create client with base URL and default headers using pingpong.create()
const client = pingpong.create({
baseURL: 'https://api.mockly.codes',
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json'
},
timeout: 5000
});
// Relative URL (uses baseURL)
const users = await client.get('/users');
// Absolute URL (ignores baseURL)
const external = await pingpong.get('https://other-api.com/data');
// Override headers for specific request
const response = await client.get('/users', {
headers: {
'Authorization': 'Bearer different-token'
}
});Proxy Support (Node.js)
import pingpong from '@pingpong-js/fetch';
const client = pingpong.create({
proxy: 'http://proxy.example.com:8080'
});
// All requests go through proxy
const response = await client.get('https://api.mockly.codes/data');
// Override proxy for specific request
const response2 = await pingpong.get('https://api.mockly.codes/data', {
proxy: 'http://other-proxy.com:8080'
});Redirect Handling
import pingpong from '@pingpong-js/fetch';
const client = pingpong.create({
followRedirects: true, // Default: true
maxRedirects: 10 // Default: 10
});
// Automatically follows redirects
const response = await client.get('https://api.mockly.codes/redirect');
// Disable redirects for specific request
const response2 = await pingpong.get('https://api.mockly.codes/redirect', {
followRedirects: false
});
// Limit redirects for specific request
const response3 = await pingpong.get('https://api.mockly.codes/redirect', {
maxRedirects: 5
});TypeScript Support
import pingpong, { HttpResponse } from '@pingpong-js/fetch';
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
message?: string;
}
// Type-safe with generics (v1.4.0) - response.data is typed!
const response = await pingpong.get<User>('https://api.mockly.codes/users/1');
console.log(response.data.name); // TypeScript knows this is string!
console.log(response.data.email); // Also typed!
// POST with typed response
const newUser = await pingpong.post<User>('https://api.mockly.codes/users', {
name: 'John Doe',
email: '[email protected]'
});
console.log(newUser.data.id); // TypeScript knows this is number!
// Complex nested types work too
const apiResponse = await pingpong.get<ApiResponse<User[]>>('https://api.mockly.codes/api/users');
apiResponse.data.data.forEach(user => {
console.log(user.email); // Fully typed!
});
// Check response status with type-safe methods
if (response.ok()) {
console.log('User:', response.data); // data is typed as User
}
// Legacy .json() method also supports generics
const user = await pingpong.get('https://api.mockly.codes/users/1').then(r => r.json<User>());
console.log(user.name);Error Handling
import pingpong from '@pingpong-js/fetch';
try {
const response = await pingpong.get('https://api.mockly.codes/data');
// Use chainable methods to check response
if (response.isError()) {
console.error('HTTP Error:', response.status, response.statusText);
const error = response.json();
console.error('Error details:', error);
} else if (response.ok()) {
const data = response.json();
console.log('Success!', data);
}
} catch (error) {
// Network errors, timeouts, etc.
console.error('Request failed:', error);
}
// Check for cancelled requests
const controller = new AbortController();
const response = await pingpong.get('https://api.mockly.codes/data', {
signal: controller.signal
});
if (response.status === 0 && response.statusText === 'Cancelled') {
console.log('Request was cancelled');
}🌍 Environment Detection
The library automatically detects the runtime environment and uses the optimal implementation:
- Node.js: Uses
undicifor high performance - Browser: Uses native
fetch()API
Automatic Detection (Default)
import pingpong from '@pingpong-js/fetch';
// Automatically uses the right implementationExplicit Entry Points
For bundlers like webpack, you can use specific entry points:
Browser-only (recommended for browser bundles):
import pingpong from '@pingpong-js/fetch/browser';
// Only includes browser code, no Node.js dependenciesNode.js-only:
import pingpong from '@pingpong-js/fetch/node';
// Only includes Node.js code with undiciBundler Configuration
For browser builds, the library automatically uses the browser entry point via the browser field in package.json. Most modern bundlers (Vite, Webpack, Rollup) will automatically pick this up.
Vite (automatic, no config needed):
import pingpong from '@pingpong-js/fetch';
// Automatically uses browser entry pointWebpack (optional explicit alias):
resolve: {
alias: {
'@pingpong-js/fetch': '@pingpong-js/fetch/browser'
}
}📋 Features by Environment
| Feature | Node.js | Browser | |---------|---------|---------| | HTTP Methods | ✅ | ✅ | | Custom Headers | ✅ | ✅ | | Request Body | ✅ | ✅ | | Timeout | ✅ | ✅ | | Redirects | ✅ | ✅ | | Proxy Support | ✅ | ⚠️ (logs warning) | | Binary Data | ✅ | ✅ | | AbortSignal | ✅ | ✅ | | Streaming | ✅ | ✅ | | Retry Logic | ✅ | ✅ | | JSON Parsing | ✅ | ✅ | | Credentials | ❌ | ✅ |
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide for details.
📄 License
MIT © 0xdps
🔗 Related Packages
Core Packages:
@pingpong-js/cli- Command-line HTTP client@pingpong-js/core- Core utilities and types- PingPong Extension - Browser extension
Plugin Packages (Lodash-style): Install only what you need:
@pingpong-js/plugin-cache- Smart caching with memory, localStorage, IndexedDB@pingpong-js/plugin-retry- Advanced retry with circuit breaker, rate limiting@pingpong-js/plugin-zod- Zod schema validation for type-safe responses
# Install plugins separately
npm install @pingpong-js/plugin-cache
npm install @pingpong-js/plugin-retry
npm install @pingpong-js/plugin-zod💬 Support
For questions, issues, or feature requests, please open an issue or contact [email protected].
