UNPKG

fetchff

Version:

<div align="center"> <img src="./docs/logo.png" alt="logo" width="380"/>

922 lines (660 loc) 160 kB
<div align="center"> <img src="./docs/logo.png" alt="logo" width="380"/> <h4 align="center">Fast, lightweight (~5 KB gzipped) and reusable data fetching</h4> <i>"fetchff" stands for "fetch fast & flexibly"</i> [npm-url]: https://npmjs.org/package/fetchff [npm-image]: https://img.shields.io/npm/v/fetchff.svg [![NPM version][npm-image]][npm-url] [![Blazing Fast](https://badgen.now.sh/badge/speed/blazing%20%F0%9F%94%A5/green)](https://github.com/MattCCC/fetchff) [![Code Coverage](https://img.shields.io/badge/coverage-96.93-green)](https://github.com/MattCCC/fetchff) [![npm downloads](https://img.shields.io/npm/dm/fetchff.svg?color=lightblue)](http://npm-stat.com/charts.html?package=fetchff) [![gzip size](https://img.shields.io/bundlephobia/minzip/fetchff)](https://bundlephobia.com/result?p=fetchff) [![snyk](https://snyk.io/test/github/MattCCC/fetchff/badge.svg)](https://security.snyk.io/package/npm/fetchff) </div> ## Why? This is a high level library to extend the functionality of native fetch() with everything necessary and no overhead, so to wrap and reuse common patterns and functionalities in a simple and declarative manner. It is designed to be used in high-throughput, high-performance applications. Also, managing multitude of API connections in large applications can be complex, time-consuming and hard to scale. `fetchff` simplifies the process by offering a simple, declarative approach to API handling using Repository Pattern. It reduces the need for extensive setup, middlewares, retries, custom caching, and heavy plugins, and lets developers focus on data handling and application logic. <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> **Some of challenges with Native Fetch that `fetchff` solves:** - **Error Status Handling:** Fetch does not throw errors for HTTP error statuses, making it difficult to distinguish between successful and failed requests based on status codes alone. - **Error Visibility:** Error responses with status codes like 404 or 500 are not automatically propagated as exceptions, which can lead to inconsistent error handling. - **No Built-in Retry Mechanism:** Native `fetch()` lacks built-in support for retrying requests. Developers need to implement custom retry logic to handle transient errors or intermittent failures, which can be cumbersome and error-prone. - **Network Errors Handling:** Native `fetch()` only rejects the Promise for network errors or failure to reach the server. Issues such as timeout errors or server unavailability do not trigger rejection by default, which can complicate error management. - **Limited Error Information:** The error information provided by native `fetch()` is minimal, often leaving out details such as the request headers, status codes, or response bodies. This can make debugging more difficult, as there's limited visibility into what went wrong. - **Lack of Interceptors:** Native `fetch()` does not provide a built-in mechanism for intercepting requests or responses. Developers need to manually manage request and response processing, which can lead to repetitive code and less maintainable solutions. - **No Built-in Caching:** Native `fetch()` does not natively support caching of requests and responses. Implementing caching strategies requires additional code and management, potentially leading to inconsistencies and performance issues. To address these challenges, the `fetchf()` provides several enhancements: 1. **Consistent Error Handling:** - In JavaScript, the native `fetch()` function does not reject the Promise for HTTP error statuses such as 404 (Not Found) or 500 (Internal Server Error). Instead, `fetch()` resolves the Promise with a `Response` object, where the `ok` property indicates the success of the request. If the request encounters a network error or fails due to other issues (e.g., server downtime), `fetch()` will reject the Promise. - The `fetchff` plugin aligns error handling with common practices and makes it easier to manage errors consistently by rejecting erroneous status codes. 2. **Enhanced Retry Mechanism:** - **Retry Configuration:** You can configure the number of retries, delay between retries, and exponential backoff for failed requests. This helps to handle transient errors effectively. - **Custom Retry Logic:** The `shouldRetry` asynchronous function allows for custom retry logic based on the error from `response.error` and attempt count, providing flexibility to handle different types of failures. - **Retry Conditions:** Errors are only retried based on configurable retry conditions, such as specific HTTP status codes or error types. 3. **Improved Error Visibility:** - **Error Wrapping:** The `createApiFetcher()` and `fetchf()` wrap errors in a custom `ResponseError` class, which provides detailed information about the request and response. This makes debugging easier and improves visibility into what went wrong. 4. **Extended settings:** - Check Settings table for more information about all settings. </details> ## ✔️ Benefits ✅ **Lightweight:** Minimal code footprint of ~4KB gzipped for managing extensive APIs. ✅ **High-Performance**: Optimized for speed and efficiency, ensuring fast and reliable API interactions. ✅ **Secure:** Secure by default rather than "permissive by default", with built-in sanitization mechanisms. ✅ **Immutable:** Every request has its own instance. ✅ **Isomorphic:** Compatible with Node.js, Deno and modern browsers. ✅ **Type Safe:** Strongly typed and written in TypeScript. ✅ **Scalable:** Easily scales from a few calls to complex API networks with thousands of APIs. ✅ **Tested:** Battle tested in large projects, fully covered by unit tests. ✅ **Customizable:** Fully compatible with a wide range configuration options, allowing for flexible and detailed request customization. ✅ **Responsible Defaults:** All settings are opt-in. ✅ **Framework Independent**: Pure JavaScript solution, compatible with any framework or library, both client and server side. ✅ **Browser and Node.js 18+ Compatible:** Works flawlessly in both modern browsers and Node.js environments. ✅ **Maintained:** Since 2021 publicly through Github. ## ✔️ Features - **Smart Retry Mechanism**: Features exponential backoff for intelligent error handling and retry mechanisms. - **Request Deduplication**: Set the time during which requests are deduplicated (treated as same request). - **Cache Management**: Dynamically manage cache with configurable expiration, custom keys, and selective invalidation. - **Network Revalidation**: Automatically revalidate data on window focus and network reconnection for fresh data. - **Dynamic URLs Support**: Easily manage routes with dynamic parameters, such as `/user/:userId`. - **Error Handling**: Flexible error management at both global and individual request levels. - **Request Cancellation**: Utilizes `AbortController` to cancel previous requests automatically. - **Adaptive Timeouts**: Smart timeout adjustment based on connection speed for optimal user experience. - **Fetching Strategies**: Handle failed requests with various strategies - promise rejection, silent hang, soft fail, or default response. - **Requests Chaining**: Easily chain multiple requests using promises for complex API interactions. - **Native `fetch()` Support**: Utilizes the built-in `fetch()` API, providing a modern and native solution for making HTTP requests. - **Custom Interceptors**: Includes `onRequest`, `onResponse`, and `onError` interceptors for flexible request and response handling. ## ✔️ Install [![NPM](https://nodei.co/npm/fetchff.png)](https://npmjs.org/package/fetchff) Using NPM: ```bash npm install fetchff ``` Using Pnpm: ```bash pnpm install fetchff ``` Using Yarn: ```bash yarn add fetchff ``` ## ✔️ API ### Standalone usage #### `fetchf(url, config)` _Alias: `fetchff(url, config)`_ A simple function that wraps the native `fetch()` and adds extra features like retries and better error handling. Use `fetchf()` directly for quick, enhanced requests - no need to set up `createApiFetcher()`. It works independently and is easy to use in any codebase. #### Example ```typescript import { fetchf } from 'fetchff'; const { data, error } = await fetchf('/api/user-details', { timeout: 5000, cancellable: true, retry: { retries: 3, delay: 2000 }, // Specify some other settings here... The fetch() settings work as well... }); ``` ### Global Configuration #### `getDefaultConfig()` <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> Returns the current global default configuration used for all requests. This is useful for inspecting or debugging the effective global settings. ```typescript import { getDefaultConfig } from 'fetchff'; // Retrieve the current global default config const config = getDefaultConfig(); console.log('Current global fetchff config:', config); ``` </details> #### `setDefaultConfig(customConfig)` <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> Allows you to globally override the default configuration for all requests. This is useful for setting application-wide defaults like timeouts, headers, or retry policies. ```typescript import { setDefaultConfig } from 'fetchff'; // Set global defaults for all requests setDefaultConfig({ timeout: 10000, // 10 seconds for all requests headers: { Authorization: 'Bearer your-token', }, retry: { retries: 2, delay: 1500, }, }); // All subsequent requests will use these defaults const { data } = await fetchf('/api/data'); // Uses 10s timeout and retry config ``` </details> ### Instance with many API endpoints #### `createApiFetcher(config)` <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> It is a powerful factory function for creating API fetchers with advanced features. It provides a convenient way to configure and manage multiple API endpoints using a declarative approach. This function offers integration with retry mechanisms, error handling improvements, and all the other settings. Unlike traditional methods, `createApiFetcher()` allows you to set up and use API endpoints efficiently with minimal boilerplate code. #### Example ```typescript import { createApiFetcher } from 'fetchff'; // Create some endpoints declaratively const api = createApiFetcher({ baseURL: 'https://example.com/api', endpoints: { getUser: { url: '/user-details/:id/', method: 'GET', // Each endpoint accepts all settings declaratively retry: { retries: 3, delay: 2000 }, timeout: 5000, cancellable: true, }, // Define more endpoints as needed }, // You can set all settings globally strategy: 'softFail', // no try/catch required in case of errors }); // Make a GET request to http://example.com/api/user-details/2/?rating[]=1&rating[]=2 const { data, error } = await api.getUser({ params: { rating: [1, 2] }, // Passed arrays, objects etc. will be parsed automatically urlPathParams: { id: 2 }, // Replace :id with 2 in the URL }); ``` #### Multiple API Specific Settings All the Request Settings can be directly used in the function as global settings for all endpoints. They can be also used within the `endpoints` property (on per-endpoint basis). The exposed `endpoints` property is as follows: - **`endpoints`**: Type: `EndpointsConfig<EndpointTypes>` List of your endpoints. Each endpoint is an object that accepts all the Request Settings (see the Basic Settings below). The endpoints are required to be specified. #### How It Works The `createApiFetcher()` automatically creates and returns API methods based on the `endpoints` object provided. It also exposes some extra methods and properties that are useful to handle global config, dynamically add and remove endpoints etc. #### `api.yourEndpoint(requestConfig)` Where `yourEndpoint` is the name of your endpoint, the key from `endpoints` object passed to the `createApiFetcher()`. **`requestConfig`** (optional) `object` - To have more granular control over specific endpoints you can pass Request Config for particular endpoint. Check <b>Basic Settings</b> below for more information. Returns: <b>Response Object</b> (see below). #### `api.request(endpointNameOrUrl, requestConfig)` The `api.request()` helper function is a versatile method provided for making API requests with customizable configurations. It allows you to perform HTTP requests to any endpoint defined in your API setup and provides a straightforward way to handle queries, path parameters, and request configurations dynamically. ##### Example ```typescript import { createApiFetcher } from 'fetchff'; const api = createApiFetcher({ apiUrl: 'https://example.com/api', endpoints: { updateUser: { url: '/update-user/:id', method: 'POST', }, // Define more endpoints as needed }, }); // Using api.request to make a POST request const { data, error } = await api.request('updateUser', { body: { name: 'John Doe', // Data Payload }, urlPathParams: { id: '123', // URL Path Param :id will be replaced with 123 }, }); // Using api.request to make a GET request to an external API const { data, error } = await api.request('https://example.com/api/user', { params: { name: 'John Smith', // Query Params }, }); ``` #### `api.config` You can access `api.config` property directly to modify global headers and other settings on the fly. This is a property, not a function. #### `api.endpoints` You can access `api.endpoints` property directly to modify the endpoints list. This can be useful if you want to append or remove global endpoints. This is a property, not a function. </details> ### Advanced Utilities <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> #### Cache Management ##### `mutate(key, newData, settings)` Programmatically update cached data without making a network request. Useful for optimistic updates or reflecting changes from other operations. **Parameters:** - `key` (string): The cache key to update - `newData` (any): The new data to store in cache - `settings` (object, optional): Configuration options - `revalidate` (boolean): Whether to trigger background revalidation after update ```typescript import { mutate } from 'fetchff'; // Update cache for a specific cache key await mutate('/api/users', newUserData); // Update with options await mutate('/api/users', updatedData, { revalidate: true, // Trigger background revalidation }); ``` ##### `getCache(key)` Directly retrieve cached data for a specific cache key. Useful for reading the current cached response without triggering a network request. **Parameters:** - `key` (string): The cache key to retrieve (equivalent to `cacheKey` from request config or `config.cacheKey` from response object) **Returns:** The cached response object, or `null` if not found ```typescript import { getCache } from 'fetchff'; // Get cached data for a specific key assuming you set {cacheKey: ''/api/user-profile'} in config const cachedResponse = getCache('/api/user-profile'); if (cachedResponse) { console.log('Cached user profile:', cachedResponse.data); } ``` ##### `setCache(key, response, ttl, staleTime)` Directly set cache data for a specific key. Unlike `mutate()`, this doesn't trigger revalidation by default. This is a low level function to directly set cache data based on particular key. If unsure, use the `mutate()` with `revalidate: false` instead. **Parameters:** - `key` (string): The cache key to set. It must match the cache key of the request. - `response` (any): The full response object to store in cache - `ttl` (number, optional): Time to live for the cache entry, in seconds. Determines how long the cached data remains valid before expiring. If not specified, the default `0` value will be used (discard cache immediately), if `-1` specified then the cache will be held until manually removed using `deleteCache(key)` function. - `staleTime` (number, optional): Duration, in seconds, for which cached data is considered "fresh" before it becomes eligible for background revalidation. If not specified, the default stale time applies. ```typescript import { setCache } from 'fetchff'; // Set cache data with custom ttl and staleTime setCache('/api/user-profile', userData, 600, 60); // Cache for 10 minutes, fresh for 1 minute // Set cache for specific endpoint infinitely setCache('/api/user-settings', userSettings, -1); ``` ##### `deleteCache(key)` Remove cached data for a specific cache key. Useful for cache invalidation when you know data is stale. **Parameters:** - `key` (string): The cache key to delete ```typescript import { deleteCache } from 'fetchff'; // Delete specific cache entry deleteCache('/api/user-profile'); // Delete cache after user logout const logout = () => { deleteCache('/api/user/*'); // Delete all user-related cache }; ``` #### Revalidation Management ##### `revalidate(key, isStaleRevalidation)` Manually trigger revalidation for a specific cache entry, forcing a fresh network request to update the cached data. **Parameters:** - `key` (string): The cache key to revalidate - `isStaleRevalidation` (boolean, optional): Whether this is a background revalidation that doesn't mark as in-flight ```typescript import { revalidate } from 'fetchff'; // Revalidate specific cache entry await revalidate('/api/user-profile'); // Revalidate with custom cache key await revalidate('custom-cache-key'); // Background revalidation (doesn't mark as in-flight) await revalidate('/api/user-profile', true); ``` ##### `revalidateAll(type, isStaleRevalidation)` Trigger revalidation for all cache entries associated with a specific event type (focus or online). **Parameters:** - `type` (string): The revalidation event type ('focus' or 'online') - `isStaleRevalidation` (boolean, optional): Whether this is a background revalidation ```typescript import { revalidateAll } from 'fetchff'; // Manually trigger focus revalidation for all relevant entries revalidateAll('focus'); // Manually trigger online revalidation for all relevant entries revalidateAll('online'); ``` ##### `removeRevalidators(type)` Clean up revalidation event listeners for a specific event type. Useful for preventing memory leaks when you no longer need automatic revalidation. **Parameters:** - `type` (string): The revalidation event type to remove ('focus' or 'online') ```typescript import { removeRevalidators } from 'fetchff'; // Remove all focus revalidation listeners removeRevalidators('focus'); // Remove all online revalidation listeners removeRevalidators('online'); // Typically called during cleanup // e.g., in React useEffect cleanup or when unmounting components ``` #### Pub/Sub System ##### `subscribe(key, callback)` Subscribe to cache updates and data changes. Receive notifications when specific cache entries are updated. **Parameters:** - `key` (string): The cache key to subscribe to - `callback` (function): Function called when cache is updated - `response` (any): The full response object **Returns:** Function to unsubscribe from updates ```typescript import { subscribe } from 'fetchff'; // Subscribe to cache changes for a specific key const unsubscribe = subscribe('/api/user-data', (response) => { console.log('Cache updated with response:', response); console.log('Response data:', response.data); console.log('Response status:', response.status); }); // Clean up subscription when no longer needed unsubscribe(); ``` #### Request Management ##### `abortRequest(key, error)` Programmatically abort in-flight requests for a specific cache key or URL pattern. **Parameters:** - `key` (string): The cache key or URL pattern to abort - `error` (Error, optional): Custom error to throw for aborted requests ```typescript import { abortRequest } from 'fetchff'; // Abort specific request by cache key abortRequest('/api/slow-operation'); // Useful for cleanup when component unmounts or route changes const cleanup = () => { abortRequest('/api/user-dashboard'); }; ``` #### Network Detection ##### `isSlowConnection()` Check if the user is on a slow network connection (2G/3G). Useful for adapting application behavior based on connection speed. **Parameters:** None **Returns:** Boolean indicating if connection is slow ```typescript import { isSlowConnection } from 'fetchff'; // Check connection speed and adapt behavior if (isSlowConnection()) { console.log('User is on a slow connection'); // Reduce image quality, disable auto-refresh, etc. } // Use in conditional logic const shouldAutoRefresh = !isSlowConnection(); const imageQuality = isSlowConnection() ? 'low' : 'high'; ``` </details> ## 🛠️ Plugin API Architecture <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> ![Example SVG](./docs/api-architecture.png) </details> ## ⚙️ Basic Settings <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> You can pass the settings: - globally for all requests when calling `createApiFetcher()` - per-endpoint basis defined under `endpoints` in global config when calling `createApiFetcher()` - per-request basis when calling `fetchf()` (second argument of the function) or in the `api.yourEndpoint()` (third argument) You can also use all native [`fetch()` settings](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters). | | Type | Default | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | baseURL<br>(alias: apiUrl) | `string` | `undefined` | Your API base url. | | url | `string` | `undefined` | URL path e.g. /user-details/get | | method | `string` | `'GET'` | Default request method e.g. GET, POST, DELETE, PUT etc. All methods are supported. | | params | `object`<br>`URLSearchParams`<br>`NameValuePair[]` | `undefined` | Query Parameters - a key-value pairs added to the URL to send extra information with a request. If you pass an object, it will be automatically converted. It works with nested objects, arrays and custom data structures similarly to what `jQuery` used to do in the past. If you use `createApiFetcher()` then it is the first argument of your `api.yourEndpoint()` function. You can still pass configuration in 3rd argument if want to.<br><br>You can pass key-value pairs where the values can be strings, numbers, or arrays. For example, if you pass `{ foo: [1, 2] }`, it will be automatically serialized into `foo[]=1&foo[]=2` in the URL. | | body<br>(alias: data) | `object`<br>`string`<br>`FormData`<br>`URLSearchParams`<br>`Blob`<br>`ArrayBuffer`<br>`ReadableStream` | `undefined` | The body is the data sent with the request, such as JSON, text, or form data, included in the request payload for POST, PUT, or PATCH requests. | | urlPathParams | `object` | `undefined` | It lets you dynamically replace segments of your URL with specific values in a clear and declarative manner. This feature is especially handy for constructing URLs with variable components or identifiers.<br><br>For example, suppose you need to update user details and have a URL template like `/user-details/update/:userId`. With `urlPathParams`, you can replace `:userId` with a real user ID, such as `123`, resulting in the URL `/user-details/update/123`. | | flattenResponse | `boolean` | `false` | When set to `true`, this option flattens the nested response data. This means you can access the data directly without having to use `response.data.data`. It works only if the response structure includes a single `data` property. | | select | `(data: any) => any` | `undefined` | Function to transform or select a subset of the response data before it is returned. Called with the raw response data and should return the transformed data. Useful for mapping, picking, or shaping the response. | | defaultResponse | `any` | `null` | Default response when there is no data or when endpoint fails depending on the chosen `strategy` | | withCredentials | `boolean` | `false` | Indicates whether credentials (such as cookies) should be included with the request. This equals to `credentials: "include"` in native `fetch()`. In Node.js, cookies are not managed automatically. Use a fetch polyfill or library that supports cookies if needed. | | timeout | `number` | `30000` / `60000` | You can set a request timeout in milliseconds. **Default is adaptive**: 30 seconds (30000 ms) for normal connections, 60 seconds (60000 ms) on slow connections (2G/3G). The timeout option applies to each individual request attempt including retries and polling. `0` means that the timeout is disabled. | | dedupeTime | `number` | `0` | Time window, in milliseconds, during which identical requests are deduplicated (treated as same request). If set to `0`, deduplication is disabled. | | cacheTime | `number` | `undefined` | Specifies the duration, in seconds, for which a cache entry is considered "fresh." Once this time has passed, the entry is considered stale and may be refreshed with a new request. Set to -1 for indefinite cache. By default no caching. | | staleTime | `number` | `undefined` | Specifies the duration, in seconds, for which cached data is considered "fresh." During this period, cached data will be returned immediately, but a background revalidation (network request) will be triggered to update the cache. If set to `0`, background revalidation is disabled and revalidation is triggered on every access. | | refetchOnFocus | `boolean` | `false` | When set to `true`, automatically revalidates (refetches) data when the browser window regains focus. **Note: This bypasses the cache and always makes a fresh network request** to ensure users see the most up-to-date data when they return to your application from another tab or window. Particularly useful for applications that display real-time or frequently changing data, but should be used judiciously to avoid unnecessary network traffic. | | refetchOnReconnect | `boolean` | `false` | When set to `true`, automatically revalidates (refetches) data when the browser regains internet connectivity after being offline. **This uses background revalidation to silently update data** without showing loading states to users. Helps ensure your application displays fresh data after network interruptions. Works by listening to the browser's `online` event. | | logger | `Logger` | `null` | You can additionally specify logger object with your custom logger to automatically log the errors to the console. It should contain at least `error` and `warn` functions. | | fetcher | `CustomFetcher` | `undefined` | A custom fetcher async function. By default, the native `fetch()` is used. If you use your own fetcher, default response parsing e.g. `await response.json()` call will be skipped. Your fetcher should return response object / data directly. | > 📋 **Additional Settings Available:** > The table above shows the most commonly used settings. Many more advanced configuration options are available and documented in their respective sections below, including: > > - **🔄 Retry Mechanism** - `retries`, `delay`, `maxDelay`, `backoff`, `resetTimeout`, `retryOn`, `shouldRetry` > - **📶 Polling Configuration** - `pollingInterval`, `pollingDelay`, `maxPollingAttempts`, `shouldStopPolling` > - **🗄️ Cache Management** - `cacheKey`, `cacheBuster`, `skipCache`, `cacheErrors` > - **✋ Request Cancellation** - `cancellable`, `rejectCancelled` > - **🌀 Interceptors** - `onRequest`, `onResponse`, `onError`, `onRetry` > - **🔍 Error Handling** - `strategy` ### Performance Implications of Settings Understanding the performance impact of different settings helps you optimize for your specific use case: #### **High-Performance Settings** **Minimize Network Requests:** ```typescript // Aggressive caching for static data const staticConfig = { cacheTime: 3600, // 1 hour cache staleTime: 1800, // 30 minutes freshness dedupeTime: 10000, // 10 seconds deduplication }; // Result: 90%+ reduction in network requests ``` **Optimize for Mobile/Slow Connections:** ```typescript const mobileOptimized = { timeout: 60000, // Longer timeout for slow connections (auto-adaptive) retry: { retries: 5, // More retries for unreliable connections delay: 2000, // Longer initial delay (auto-adaptive) backoff: 2.0, // Aggressive backoff }, cacheTime: 900, // Longer cache on mobile }; ``` #### **Memory vs Network Trade-offs** **Memory-Efficient (Low Cache):** ```typescript const memoryEfficient = { cacheTime: 60, // Short cache (1 minute) staleTime: undefined, // No stale-while-revalidate dedupeTime: 1000, // Short deduplication }; // Pros: Low memory usage // Cons: More network requests, slower perceived performance ``` **Network-Efficient (High Cache):** ```typescript const networkEfficient = { cacheTime: 1800, // Long cache (30 minutes) staleTime: 300, // 5 minutes stale-while-revalidate dedupeTime: 5000, // Longer deduplication }; // Pros: Fewer network requests, faster user experience // Cons: Higher memory usage, potentially stale data ``` #### **Feature Performance Impact** | Feature | Performance Impact | Best Use Case | | -------------------------- | ----------------------------------- | ------------------------------------------- | | **Caching** | ⬇️ 70-90% fewer requests | Static or semi-static data | | **Deduplication** | ⬇️ 50-80% fewer concurrent requests | High-traffic applications | | **Stale-while-revalidate** | ⬆️ 90% faster perceived loading | Dynamic data that tolerates brief staleness | | **Request cancellation** | ⬇️ Reduced bandwidth waste | Search-as-you-type, rapid navigation | | **Retry mechanism** | ⬆️ 95%+ success rate | Mission-critical operations | | **Polling** | ⬆️ Real-time updates | Live data monitoring | #### **Adaptive Performance by Connection** FetchFF automatically adapts timeouts and retry delays based on connection speed: ```typescript // Automatic adaptation (no configuration needed) const adaptiveRequest = fetchf('/api/data'); // On fast connections (WiFi/4G): // - timeout: 30 seconds // - retry delay: 1 second → 1.5s → 2.25s... // - max retry delay: 30 seconds // On slow connections (2G/3G): // - timeout: 60 seconds // - retry delay: 2 seconds → 3s → 4.5s... // - max retry delay: 60 seconds ``` #### **Performance Patterns** **Progressive Loading (Best UX):** ```typescript // Layer 1: Instant response with cache const quickData = await fetchf('/api/summary', { cacheTime: 300, staleTime: 60, }); // Layer 2: Background enhancement fetchf('/api/detailed-data', { strategy: 'silent', cacheTime: 600, onResponse(response) { updateUIWithDetailedData(response.data); }, }); ``` **Bandwidth-Conscious Loading:** ```typescript // Check connection before expensive operations import { isSlowConnection } from 'fetchff'; const loadUserDashboard = async () => { const isSlowConn = isSlowConnection(); // Essential data always loads const userData = await fetchf('/api/user', { cacheTime: isSlowConn ? 600 : 300, // Longer cache on slow connections }); // Optional data only on fast connections if (!isSlowConn) { fetchf('/api/user/analytics', { strategy: 'silent' }); fetchf('/api/user/recommendations', { strategy: 'silent' }); } }; ``` #### **Performance Monitoring** Track key metrics to optimize your settings: ```typescript const performanceConfig = { onRequest(config) { console.time(`request-${config.url}`); }, onResponse(response) { console.timeEnd(`request-${response.config.url}`); // Track cache hit rate if (response.fromCache) { incrementMetric('cache.hits'); } else { incrementMetric('cache.misses'); } }, onError(error) { incrementMetric('requests.failed'); console.warn('Request failed:', error.config.url, error.status); }, }; ``` > ℹ️ **Note:** This is just an example. You need to implement the `incrementMetric` function yourself to record or report performance metrics as needed in your application. </details> ## 🏷️ Headers <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> `fetchff` provides robust support for handling HTTP headers in your requests. You can configure and manipulate headers at both global and per-request levels. Here’s a detailed overview of how to work with headers using `fetchff`. **Note:** Header keys are case-sensitive when specified in request objects. Ensure that the keys are provided in the correct case to avoid issues with header handling. ### Setting Headers Globally You can set default headers that will be included in all requests made with a specific `createApiFetcher` instance. This is useful for setting common headers like authentication tokens or content types. #### Example: Setting Headers Globally ```typescript import { createApiFetcher } from 'fetchff'; const api = createApiFetcher({ baseURL: 'https://api.example.com/', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer YOUR_TOKEN', }, // other configurations }); ``` ### Setting Per-Request Headers In addition to global default headers, you can also specify headers on a per-request basis. This allows you to override global headers or set specific headers for individual requests. #### Example: Setting Per-Request Headers ```typescript import { fetchf } from 'fetchff'; // Example of making a GET request with custom headers const { data } = await fetchf('https://api.example.com/endpoint', { headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', 'Custom-Header': 'CustomValue', }, }); ``` ### Default Headers The `fetchff` plugin automatically injects a set of default headers into every request. These default headers help ensure that requests are consistent and include necessary information for the server to process them correctly. - **`Accept`**: `application/json, text/plain, */*` Indicates the media types that the client is willing to receive from the server. This includes JSON, plain text, and any other types. - **`Accept-Encoding`**: `gzip, deflate, br` Specifies the content encoding that the client can understand, including gzip, deflate, and Brotli compression. > ⚠️ **Accept-Encoding in Node.js:** > In Node.js, decompression is handled by the fetch implementation, and users should ensure their environment supports the encodings. - **`Content-Type`**: Set automatically based on the request body type: - For JSON-serializable bodies (objects, arrays, etc.): `application/json; charset=utf-8` - For `URLSearchParams`: `application/x-www-form-urlencoded` - For `ArrayBuffer`/typed arrays: `application/octet-stream` - For `FormData`, `Blob`, `File`, or `ReadableStream`: **Not set** as the header is handled automatically by the browser and by Node.js 18+ native fetch. The `Content-Type` header is **never overridden** if you set it manually. **Summary:** You only need to set headers manually if you want to override these defaults. Otherwise, `fetchff` will handle the correct headers for most use cases, including advanced scenarios like file uploads, form submissions, and binary data. </details> ## 🌀 Interceptors <details> <summary><span style="cursor:pointer">Click to expand</span></summary> <br> Interceptor functions can be provided to customize the behavior of requests and responses. These functions are invoked at different stages of the request lifecycle and allow for flexible handling of requests, responses, and errors. ### Example ```typescript const { data } = await fetchf('https://api.example.com/', { onRequest(config) { // Add a custom header before sending the request config.headers['Authorization'] = 'Bearer your-token'; }, onResponse(response) { // Log the response status console.log(`Response Status: ${response.status}`); }, onError(error, config) { // Handle errors and log the request config console.error('Request failed:', error); console.error('Request config:', config); }, onRetry(response, attempt) { // Log retry attempts for monitoring and debugging console.warn( `Retrying request (attempt ${attempt + 1}):`, response.config.url, ); // Modify config for the upcoming retry request response.config.headers['Authorization'] = 'Bearer your-new-token'; // Log error details for failed attempts if (response.error) { console.warn( `Retry reason: ${response.error.status} - ${response.error.statusText}`, ); } // You can implement custom retry logic or monitoring here // For example, send retry metrics to your analytics service }, retry: { retries: 3, delay: 1000, backoff: 1.5, }, }); ``` ### Configuration The following options are available for configuring interceptors in the `fetchff` settings: - **`onRequest(config) => config`**: Type: `RequestInterceptor | RequestInterceptor[]` A function or an array of functions that are invoked before sending a request. Each function receives the request configuration object as its argument, allowing you to modify request parameters, headers, or other settings. _Default:_ `undefined` (no modification). - **`onResponse(response) => response`**: Type: `ResponseInterceptor | ResponseInterceptor[]` A function or an array of functions that are invoked when a response is received. Each function receives the full response object, enabling you to process the response, handle status codes, or parse data as needed. _Default:_ `undefined` (no modification). - **`onError(error) => error`**: Type: `ErrorInterceptor | ErrorInterceptor[]` A function or an array of functions that handle errors when a request fails. Each function receives the error and request configuration as arguments, allowing you to implement custom error handling logic or logging. _Default:_ `undefined` (no modification). - **`onRetry(response, attempt) => response`**: Type: `RetryInterceptor | RetryInterceptor[]` A function or an array of functions that are invoked before each retry attempt. Each function receives the response object (containing error information) and the current attempt number as arguments, allowing you to implement custom retry logging, monitoring, or conditional retry logic. _Default:_ `undefined` (no retry interception). All interceptors are asynchronous and can modify the provided config or response objects. You don't have to return a value, but if you do, any returned properties will be merged into the original argument. ### Interceptor Execution Order `fetchff` follows specific execution patterns for interceptor chains: #### **Request Interceptors: FIFO (First In, First Out)** Request interceptors execute in the order they are defined - from global to specific: ```typescript // Execution order: 1 → 2 → 3 → 4 const api = createApiFetcher({ onRequest: (config) => { /* 1. Global interceptor */ }, endpoints: { getData: { on