Easily create multiple complex APIs in a declarative fashion.
Managing multitude of API connections in large Frontend Application 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.
Key Benefits:
✅ Small: Minimal code footprint of ~3KB gzipped for managing extensive APIs.
✅ Secure: Secure by default rather than “permissive by default”
✅ 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.
✅ Maintained: Since 2021 publicly through Github.
/user/:userId
.AbortController
to cancel previous requests automatically.fetch()
Support: Utilizes the built-in fetch()
API, providing a modern and native solution for making HTTP requests.onRequest
, onResponse
, and onError
interceptors for flexible request and response handling.Please open an issue for future requests.
Using NPM:
npm install fetchff
Using Pnpm:
pnpm install fetchff
Using Yarn:
yarn add fetchff
fetchf()
It is a functional wrapper for fetch()
. It seamlessly enhances it with additional settings like the retry mechanism and error handling improvements. The fetchf()
can be used directly as a function, simplifying the usage and making it easier to integrate with functional programming styles. The fetchf()
makes requests independently of createApiFetcher()
settings.
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...
});
fetchff
solves: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.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.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.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.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.fetchf()
provides several enhancements: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.fetchff
plugin aligns error handling with common practices and makes it easier to manage errors consistently by rejecting erroneous status codes.shouldRetry
asynchronous function allows for custom retry logic based on the error and attempt count, providing flexibility to handle different types of failures.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.createApiFetcher()
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.
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
});
endpoints
property (on per-endpoint basis). The exposed endpoints
property is as follows:endpoints
:EndpointsConfig<EndpointsMethods>
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)
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 Basic Settings below for more information.api.request(endpointNameOrUrl, requestConfig)
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.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
api.config
property directly, so to modify global headers, and other settings on fly. Please mind it is a property, not a function.api.endpoints
api.endpoints
property directly, so to modify endpoints list. It can be useful if you want to append or remove global endpoints. Please mind it is a property, not a function.api.getInstance()
fetcher
, then this function will return the instance is created using fetcher.create()
function. Your fetcher can include anything. It will be triggering fetcher.request()
instead of native fetch() that is available by default. It gives you ultimate flexibility on how you want your requests to be made.You can pass the settings:
createApiFetcher()
endpoints
in global config when calling createApiFetcher()
fetchf()
(second argument of the function) or in the api.yourEndpoint()
(third argument)You can also use all native fetch()
settings.
Type | Default | Description | |
---|---|---|---|
baseURL (alias: apiUrl) |
string |
Your API base url. | |
url | string |
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 URLSearchParams NameValuePair[] |
{} |
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.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 (alias: data) |
object string FormData URLSearchParams Blob ArrayBuffer ReadableStream |
{} |
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 |
{} |
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. 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. |
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. |
timeout | number |
30000 |
You can set a request timeout for all requests or particular in milliseconds. |
dedupeTime | number |
1000 |
Time window, in milliseconds, during which identical requests are deduplicated (treated as single request). |
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 | FetcherInstance |
A custom adapter (an instance / object) that exposes create() function so to create instance of API Fetcher. The create() should return request() function that would be used when making the requests. The native fetch() is used if the fetcher is not provided. |
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
.createApiFetcher
instance. This is useful for setting common headers like authentication tokens or content types.typescript
import { createApiFetcher } from 'fetchff';
const api = createApiFetcher({
baseURL: 'https://api.example.com/',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer YOUR_TOKEN',
},
// other configurations
});
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',
},
});
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.Content-Type
: application/json;charset=utf-8
Accept
: application/json, text/plain, */*
Accept-Encoding
: gzip, deflate, br
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);
},
});
RequestHandler
:onRequest
:RequestInterceptor | RequestInterceptor[]
(config) => config
(no modification).onResponse
:ResponseInterceptor | ResponseInterceptor[]
(response) => response
(no modification).onError
:ErrorInterceptor | ErrorInterceptor[]
(error) => error
(no modification).onRequest
interceptors are invoked. These interceptors can modify the request configuration, such as adding headers or changing request parameters.onResponse
interceptors are called. These interceptors allow you to handle the response data, process status codes, or transform the response before it is returned to the caller.onError
interceptors are triggered. These interceptors provide a way to handle errors, such as logging or retrying requests, based on the error and the request configuration.fetch()
API function doesn’t throw exceptions for HTTP errors like 404
or 500
— it only rejects the promise if there is a network-level error (e.g. the request fails due to a DNS error, no internet connection, or CORS issues). The fetchf()
function brings consistency and lets you align the behavior depending on chosen strategy. By default, all errors are rejected.strategy
reject
: (default)try/catch
blocks to handle errors.typescript
try {
const { data } = await fetchf('https://api.example.com/', {
strategy: 'reject', // It is default so it does not really needs to be specified
});
} catch (error) {
console.error(error.status, error.statusText, error.response, error.config);
}
softFail
:error
when an error occurs and does not throw any error. This approach helps you to handle error information directly within the response’s error
object without the need for try/catch
blocks.typescript
const { data, error } = await fetchf('https://api.example.com/', {
strategy: 'softFail',
});
if (error) {
console.error(error.status, error.statusText, error.response, error.config);
}
Response Object
section below to see how error
object is structured.defaultResponse
:flattenResponse
and defaultResponse: {}
to provide sensible defaults.typescript
const { data, error } = await fetchf('https://api.example.com/', {
strategy: 'defaultResponse',
defaultResponse: {},
});
if (error) {
console.error('Request failed', data); // "data" will be equal to {} if there is an error
}
silent
:try/catch
. In case of an error, the promise will never be resolved or rejected, and any code after will never be executed. This strategy is useful for dispatching requests within asynchronous wrapper functions that do not need to be awaited. It prevents excessive usage of try/catch
or additional response data checks everywhere. It can be used in combination with onError
to handle errors separately.typescript
async function myLoadingProcess() {
const { data } = await fetchf('https://api.example.com/', {
strategy: 'silent',
});
// In case of an error nothing below will ever be executed.
console.log('This console log will not appear.');
}
myLoadingProcess();
reject
strategy, if an error occurs, the promise is rejected, and global error handling logic is triggered. You must use try/catch
to handle these errors.softFail
, the response object includes additional properties that provide details about the error without rejecting the promise. This allows you to handle error information directly within the response.defaultResponse
strategy returns a predefined default response when an error occurs. This approach prevents the promise from being rejected, allowing for default values to be used in place of error data.silent
strategy results in the promise hanging silently on error. The promise will not be resolved or rejected, and any subsequent code will not execute. This is useful for fire-and-forget requests and can be combined with onError
for separate error handling.onError
onError
option can be configured to intercept errors:typescript
const { data } = await fetchf('https://api.example.com/', {
strategy: 'softFail',
onError(error) {
// Intercept any error
console.error('Request failed', error.status, error.statusText);
},
});
typescript
interface SuccessResponseData {
bookId: string;
bookText: string;
}
interface ErrorResponseData {
errorCode: number;
errorText: string;
}
type ResponseData = SuccessResponseData | ErrorResponseData;
const { data, error } = await fetchf<ResponseData>('https://api.example.com/', {
strategy: 'softFail',
});
// Check for error here as 'data' is available for both successful and erroneous responses
if (error) {
const errorData = data as ErrorResponseData;
console.log('Request failed', errorData.errorCode, errorData.errorText);
} else {
const successData = data as SuccessResponseData;
console.log('Request successful', successData.bookText);
}
typescript
const { data } = await fetchf('https://api.example.com/', {
cacheTime: 300, // Cache is valid for 5 minutes
cacheKey: (config) => `${config.url}-${config.method}`, // Custom cache key based on URL and method
cacheBuster: (config) => config.method === 'POST', // Bust cache for POST requests
skipCache: (response, config) => response.status !== 200, // Skip caching on non-200 responses
});
cacheTime
:number
0
(no caching).cacheKey
:CacheKeyFunction
Method
, URL
, query parameters, and headers.cacheBuster
:CacheBusterFunction
POST
) bypass the cache.(config) => false
(no cache busting).skipCache
:CacheSkipFunction
200
responses.(response, config) => false
(no skipping).cacheTime
), the cached response is returned immediately. Note that when the native fetch()
setting called cache
is set to reload
the request will automatically skip the internal cache.cacheKey
function.cacheBuster
function is defined, it determines whether to invalidate and refresh the cache for specific requests. This is useful for ensuring that certain requests, such as POST
requests, always fetch new data.skipCache
function provides flexibility in deciding whether to store a response in the cache. For example, you might skip caching responses that have a 4xx
or 5xx
status code.data
fields.defaultResponse
setting.javascript
import { fetchf } from 'fetchff';
// Function to send the request
const sendRequest = () => {
// In this example, the previous requests are automatically cancelled
// You can also control "dedupeTime" setting in order to fire the requests more or less frequently
fetchf('https://example.com/api/messages/update', {
method: 'POST',
cancellable: true,
rejectCancelled: true,
});
};
// Attach keydown event listener to the input element with id "message"
document.getElementById('message')?.addEventListener('keydown', sendRequest);
cancellable
:boolean
false
true
, any ongoing previous requests to the same API endpoint will be automatically cancelled when a subsequent request is made before the first one completes. This is useful in scenarios where repeated requests are made to the same endpoint (e.g., search inputs) and only the latest response is needed, avoiding unnecessary requests to the backend.rejectCancelled
:boolean
false
cancellable
option. If set to true
, the promise of a cancelled request will be rejected. By default (false
), when a request is cancelled, instead of rejecting the promise, a defaultResponse
will be returned, allowing graceful handling of cancellation without errors.typescript
const { data } = await fetchf('https://api.example.com/', {
pollingInterval: 5000, // Poll every 5 seconds
shouldStopPolling: (response, error, attempt) => {
if (response && response.status === 200) {
return true; // Stop polling if the response status is 200 (OK)
}
if (attempt >= 10) {
return true; // Stop polling after 10 attempts
}
return false; // Continue polling otherwise
},
});
RequestHandler
:pollingInterval
:number
0
, polling is disabled. This allows you to control the frequency of requests when polling is enabled.0
(polling disabled).shouldStopPolling
:(response: any, error: any, attempt: number) => boolean
true
to stop polling, and false
to continue polling. This allows for custom logic to decide when to stop polling based on the conditions of the response or error.(response, error, attempt) => false
(polling continues indefinitely unless manually stopped).pollingInterval
is set to a non-zero value, polling begins after the initial request. The request is repeated at intervals defined by the pollingInterval
setting.shouldStopPolling
function is invoked after each polling attempt. If it returns true
, polling will stop. Otherwise, polling will continue until the condition to stop is met, or polling is manually stopped.shouldStopPolling
function provides flexibility to implement custom logic based on the response, error, or the number of attempts. This makes it easy to stop polling when the desired outcome is reached or after a maximum number of attempts.typescript
const { data } = await fetchf('https://api.example.com/', {
retry: {
retries: 3,
delay: 100,
maxDelay: 5000,
resetTimeout: true,
backoff: 1.5,
retryOn: [500, 503],
shouldRetry(error, attempt) {
// Retry on specific errors or based on custom logic
return attempt < 3; // Retry up to 3 times
},
},
});
retry
option when instantiating the RequestHandler
. You can customize the following parameters:retries
:number
0
(no retries).delay
:number
backoff
parameter.1000
(1 second).maxDelay
:number
30000
(30 seconds).backoff
:number
backoff
factor of 1.5
means each retry delay is 1.5 times the previous delay.1.5
.retryOn
:number[]
408
- Request Timeout409
- Conflict425
- Too Early429
- Too Many Requests500
- Internal Server Error502
- Bad Gateway503
- Service Unavailable504
- Gateway TimeoutshouldRetry
:RetryFunction
retryOn
configuration and the result of the shouldRetry
function.retries
).delay
value and increases exponentially based on the backoff
factor, but will not exceed the maxDelay
.fetchff
plugin automatically handles response data transformation for any instance of Response
returned by the fetch()
(or a custom fetcher
) based on the Content-Type
header, ensuring that data is parsed correctly according to its format.application/json
): Parses the response as JSON.multipart/form-data
): Parses the response as FormData
.application/octet-stream
): Parses the response as a Blob
.application/x-www-form-urlencoded
): Parses the response as FormData
.text/*
): Parses the response as plain text.Content-Type
header is missing or not recognized, the plugin defaults to attempting JSON parsing. If that fails, it will try to parse the response as text.fetchff
plugin can handle a variety of response formats, providing a flexible and reliable method for processing data from API requests.onResponse
InterceptoronResponse
interceptor to customize how the response is handled before it reaches your application. This interceptor gives you access to the raw Response
object, allowing you to transform the data or modify the response behavior based on your needs.object
.data
:ResponseData
(or your custom type passed through generic)null
or value of defaultResponse
setting, if nothing is found.error
:ResponseError<ResponseData, QueryParams, PathParams, RequestBody>
null
otherwise.name
: The name of the error, that is ResponseError
.message
: A descriptive message about the error.status
: The HTTP status code of the response (e.g., 404, 500).statusText
: The HTTP status text of the response (e.g., ‘Not Found’, ‘Internal Server Error’).request
: Details about the HTTP request that was sent (e.g., URL, method, headers).config
: The configuration object used for the request, including URL, method, headers, and query parameters.response
: The full response object received from the server, including all headers and body.config
:RequestConfig
status
:number
statusText
:string
request
:RequestConfig
config
.headers
:HeadersObject
fetch()
is attached as well.fetchff
package provides comprehensive TypeScript typings to enhance development experience and ensure type safety. Below are details on the available, exportable types for both createApiFetcher()
and fetchf()
.fetchff
package includes several generic types to handle various aspects of API requests and responses:QueryParams<ParamsType>
: Represents query parameters for requests. Can be an object, URLSearchParams
, an array of name-value pairs, or null
.BodyPayload<PayloadType>
: Represents the request body. Can be BodyInit
, an object, an array, a string, or null
.UrlPathParams<UrlParamsType>
: Represents URL path parameters. Can be an object or null
.DefaultResponse
: Default response for all requests. Default is: any
.createApiFetcher()
createApiFetcher<EndpointsMethods, EndpointsConfiguration>()
function provides a robust set of types to define and manage API interactions.EndpointsMethods
: Represents the list of API endpoints with their respective settings. It is your own interface that you can pass to this generic. It will be cross-checked against the endpoints
object in your createApiFetcher()
configuration.Endpoint<ResponseData = DefaultResponse, QueryParams = DefaultParams, PathParams = DefaultUrlParams, RequestBody = DefaultPayload>
: Represents an API endpoint function, allowing to be defined with optional query parameters, URL path parameters, request configuration (settings), and request body (data).EndpointsSettings
: Configuration for API endpoints, including query parameters, URL path parameters, and additional request configurations. Default is typeof endpoints
.RequestInterceptor
: Function to modify request configurations before they are sent.ResponseInterceptor
: Function to process responses before they are handled by the application.ErrorInterceptor
: Function to handle errors when a request fails.CreatedCustomFetcherInstance
: Represents the custom fetcher
instance created by its create()
function.fetchf()
fetchf()
function includes types that help configure and manage network requests effectively:RequestHandlerConfig
: Main configuration options for the fetchf()
function, including request settings, interceptors, and retry configurations.RetryConfig
: Configuration options for retry mechanisms, including the number of retries, delay between retries, and backoff strategies.CacheConfig
: Configuration options for caching, including cache time, custom cache keys, and cache invalidation rules.PollingConfig
: Configuration options for polling, including polling intervals and conditions to stop polling.ErrorStrategy
: Defines strategies for handling errors, such as rejection, soft fail, default response, and silent modes.__proto__
, constructor
, and prototype
from objectstypescript
// Example of protection against prototype pollution
const userInput = {
id: 123,
__proto__: { malicious: true },
};
// The sanitization happens automatically
const response = await fetchf('/api/users', {
params: userInput, // The __proto__ property will be removed
});
sanitizeObject
utilityencodeURIComponent
MAX_DEPTH
constant (default: 10)typescript
// Example of safe URL path parameter handling
const { data } = await api.getUser({
urlPathParams: {
id: 'user-id with spaces & special chars',
},
// Automatically encoded to: /users/user-id%20with%20spaces%20%26%20special%20chars
});
Feature | fetchff | ofetch | wretch | axios | native fetch() |
---|---|---|---|---|---|
Unified API Client | ✅ | — | — | — | — |
Smart Request Cache | ✅ | — | — | — | — |
Automatic Request Deduplication | ✅ | — | — | — | — |
Custom Fetching Adapter | ✅ | — | — | — | — |
Built-in Error Handling | ✅ | — | ✅ | — | — |
Customizable Error Handling | ✅ | — | ✅ | ✅ | — |
Retries with exponential backoff | ✅ | — | — | — | — |
Advanced Query Params handling | ✅ | — | — | — | — |
Custom Retry logic | ✅ | ✅ | ✅ | — | — |
Easy Timeouts | ✅ | ✅ | ✅ | ✅ | — |
Polling Functionality | ✅ | — | — | — | — |
Easy Cancellation of stale (previous) requests | ✅ | — | — | — | — |
Default Responses | ✅ | — | — | — | — |
Custom adapters (fetchers) | ✅ | — | — | ✅ | — |
Global Configuration | ✅ | — | ✅ | ✅ | — |
TypeScript Support | ✅ | ✅ | ✅ | ✅ | ✅ |
Built-in AbortController Support | ✅ | — | — | — | — |
Request Interceptors | ✅ | ✅ | ✅ | ✅ | — |
Request and Response Transformation | ✅ | ✅ | ✅ | ✅ | — |
Integration with Libraries | ✅ | ✅ | ✅ | ✅ | — |
Request Queuing | ✅ | — | — | — | — |
Multiple Fetching Strategies | ✅ | — | — | — | — |
Dynamic URLs | ✅ | — | ✅ | — | — |
Automatic Retry on Failure | ✅ | ✅ | — | ✅ | — |
Built-in Input Sanitization | ✅ | — | — | — | — |
Prototype Pollution Protection | ✅ | — | — | — | — |
Server-Side Rendering (SSR) Support | ✅ | ✅ | — | — | — |
Minimal Installation Size | 🟢 (3.3 KB) | 🟡 (6.41 KB) | 🟢 (2.21 KB) | 🔴 (13.7 KB) | 🟢 (0 KB) |
Click to expand particular examples below. You can also check examples.ts for more examples of usage.
createApiFetcher()
with all available settings.typescript
const api = createApiFetcher({
baseURL: 'https://api.example.com/',
endpoints: {
getBooks: {
url: 'books/all',
method: 'get',
cancellable: true,
// All the global settings can be specified on per-endpoint basis as well
},
},
strategy: 'reject', // Error handling strategy.
cancellable: false, // If true, cancels previous requests to same endpoint.
rejectCancelled: false, // Reject promise for cancelled requests.
flattenResponse: false, // If true, flatten nested response data.
defaultResponse: null, // Default response when there is no data or endpoint fails.
withCredentials: true, // Pass cookies to all requests.
timeout: 30000, // Request timeout in milliseconds.
dedupeTime: 1000, // Time window, in milliseconds, during which identical requests are deduplicated (treated as single request).
pollingInterval: 5000, // Interval in milliseconds between polling attempts. Setting 0 disables polling.
shouldStopPolling: (response, error, attempt) => false, // Function to determine if polling should stop based on the response. Returns true to stop polling, false to continue.
method: 'get', // Default request method.
params: {}, // Default params added to all requests.
data: {}, // Alias for 'body'. Default data passed to POST, PUT, DELETE and PATCH requests.
cacheTime: 300, // Cache is valid for 5 minutes
cacheKey: (config) => `${config.url}-${config.method}`, // Custom cache key based on URL and method
cacheBuster: (config) => config.method === 'POST', // Bust cache for POST requests
skipCache: (response, config) => response.status !== 200, // Skip caching on non-200 responses
onError(error) {
// Interceptor on error
console.error('Request failed', error);
},
async onRequest(config) {
// Interceptor on each request
console.error('Fired on each request', config);
},
async onResponse(response) {
// Interceptor on each response
console.error('Fired on each response', response);
},
logger: {
// Custom logger for logging errors.
error(...args) {
console.log('My custom error log', ...args);
},
warn(...args) {
console.log('My custom warning log', ...args);
},
},
retry: {
retries: 3, // Number of retries on failure.
delay: 1000, // Initial delay between retries in milliseconds.
backoff: 1.5, // Backoff factor for retry delay.
maxDelay: 30000, // Maximum delay between retries in milliseconds.
resetTimeout: true, // Reset the timeout when retrying requests.
retryOn: [408, 409, 425, 429, 500, 502, 503, 504], // HTTP status codes to retry on.
shouldRetry: async (error, attempts) => {
// Custom retry logic.
return (
attempts < 3 &&
[408, 500, 502, 503, 504].includes(error.response.status)
);
},
},
});
try {
// The same API config as used above, except the "endpoints" and "fetcher" and fetcher could be used as 3rd argument of the api.getBooks()
const { data } = await api.getBooks();
console.log('Request succeeded:', data);
} catch (error) {
console.error('Request ultimately failed:', error);
}
All examples below are with usage of createApiFetcher()
. You can also use fetchf()
independently.
typescript
import { createApiFetcher } from 'fetchff';
// Create fetcher instance
const api = createApiFetcher({
baseURL: 'https://example.com/api/v1',
endpoints: {
sendMessage: {
method: 'post',
url: '/send-message/:postId',
},
getMessage: {
url: '/get-message/',
// Change baseURL to external for this endpoint onyl
baseURL: 'https://externalprovider.com/api/v2',
},
},
});
// Make a wrapper function and call your API
async function sendAndGetMessage() {
await api.sendMessage({
body: { message: 'Text' },
urlPathParams: { postId: 1 },
});
const { data } = await api.getMessage({
params: { postId: 1 },
});
}
// Invoke your wrapper function
sendAndGetMessage();
typescript
// books.d.ts
interface Book {
id: number;
title: string;
rating: number;
}
interface Books {
books: Book[];
totalResults: number;
}
interface BookQueryParams {
newBook: boolean;
}
interface BookPathParams {
bookId?: number;
}
typescript
// api.ts
import type { Endpoint } from 'fetchff';
import { createApiFetcher } from 'fetchff';
const endpoints = {
fetchBooks: {
url: 'books',
},
fetchBook: {
url: 'books/:bookId',
},
};
// No need to specify all endpoints types. For example, the "fetchBooks" is inferred automatically.
interface EndpointsList {
fetchBook: Endpoint<Book, BookQueryParams, BookPathParams>;
}
type EndpointsConfiguration = typeof endpoints;
const api = createApiFetcher<EndpointsList, EndpointsConfiguration>({
apiUrl: 'https://example.com/api/',
endpoints,
});
typescript
const book = await api.fetchBook({
params: { newBook: true },
urlPathParams: { bookId: 1 },
});
// Will return an error since "rating" does not exist in "BookQueryParams"
const anotherBook = await api.fetchBook({ params: { rating: 5 } });
// You can also pass generic type directly to the request
const books = await api.fetchBooks<Books>();
typescript
import { createApiFetcher } from 'fetchff';
const endpoints = {
getPosts: {
url: '/posts/:subject',
},
getUser: {
// Generally there is no need to specify method: 'get' for GET requests as it is default one. It can be adjusted using global "method" setting
method: 'get',
url: '/user-details',
},
updateUserDetails: {
method: 'post',
url: '/user-details/update/:userId',
strategy: 'defaultResponse',
},
};
interface EndpointsList {
getPosts: Endpoint<PostsResponse, PostsQueryParams, PostsPathParams>;
}
type EndpointsConfiguration = typeof endpoints;
const api = createApiFetcher<EndpointsList, EndpointsConfiguration>({
apiUrl: 'https://example.com/api',
endpoints,
onError(error) {
console.log('Request failed', error);
},
headers: {
'my-auth-key': 'example-auth-key-32rjjfa',
},
});
// Fetch user data - "data" will return data directly
// GET to: http://example.com/api/user-details?userId=1&ratings[]=1&ratings[]=2
const { data } = await api.getUser({ params: { userId: 1, ratings: [1, 2] } });
// Fetch posts - "data" will return data directly
// GET to: http://example.com/api/posts/myTestSubject?additionalInfo=something
const { data } = await api.getPosts({
params: { additionalInfo: 'something' },
urlPathParams: { subject: 'test' },
});
// Send POST request to update userId "1"
await api.updateUserDetails({
body: { name: 'Mark' },
urlPathParams: { userId: 1 },
});
// Send POST request to update array of user ratings for userId "1"
await api.updateUserDetails({
body: { name: 'Mark', ratings: [1, 2] },
urlPathParams: { userId: 1 },
});
typescript
import { createApiFetcher, RequestConfig, FetchResponse } from 'fetchff';
// Define the custom fetcher object
const customFetcher = {
create() {
// Create instance here. It will be called at the beginning of every request.
return {
// This function will be called whenever a request is being fired.
request: async (config: RequestConfig): Promise<FetchResponse> => {
// Implement your custom fetch logic here
const response = await fetch(config.url, config);
// Optionally, process or transform the response
return response;
},
};
},
};
// Create the API fetcher with the custom fetcher
const api = createApiFetcher({
baseURL: 'https://api.example.com/',
retry: retryConfig,
fetcher: customFetcher, // Provide the custom fetcher object directly
endpoints: {
getBooks: {
url: 'books/all',
method: 'get',
cancellable: true,
// All the global settings can be specified on per-endpoint basis as well
},
},
});
reject
(default)typescript
import { createApiFetcher } from 'fetchff';
const api = createApiFetcher({
apiUrl: 'https://example.com/api',
endpoints: {
sendMessage: {
method: 'post',
url: '/send-message/:postId',
strategy: 'reject', // It is a default strategy so it does not really need to be here
},
},
});
async function sendMessage() {
try {
await api.sendMessage({
body: { message: 'Text' },
urlPathParams: { postId: 1 },
});
console.log('Message sent successfully');
} catch (error) {
console.log(error);
}
}
sendMessage();
softFail
typescript
import { createApiFetcher } from 'fetchff';
const api = createApiFetcher({
apiUrl: 'https://example.com/api',
endpoints: {
sendMessage: {
method: 'post',
url: '/send-message/:postId',
strategy: 'softFail', // Returns a response object with additional error details without rejecting the promise.
},
},
});
async function sendMessage() {
const { data, error } = await api.sendMessage({
body: { message: 'Text' },
urlPathParams: { postId: 1 },
});
if (error) {
console.error('Request Error', error);
} else {
console.log('Message sent successfully');
}
}
sendMessage();
defaultResponse
typescript
import { createApiFetcher } from 'fetchff';
const api = createApiFetcher({
apiUrl: 'https://example.com/api',
endpoints: {
sendMessage: {
method: 'post',
url: '/send-message/:postId',
// You can also specify strategy and other settings in global list of endpoints, but just for this endpoint
// strategy: 'defaultResponse',
},
},
});
async function sendMessage() {
const { data } = await api.sendMessage({
body: { message: 'Text' },
urlPathParams: { postId: 1 },
strategy: 'defaultResponse',
// null is a default setting, you can change it to empty {} or anything
// defaultResponse: null,
onError(error) {
// Callback is still triggered here
console.log(error);
},
});
if (data === null) {
// Because of the strategy, if API call fails, it will just return null
return;
}
// You can do something with the response here
console.log('Message sent successfully');
}
sendMessage();
silent
typescript
import { createApiFetcher } from 'fetchff';
const api = createApiFetcher({
apiUrl: 'https://example.com/api',
endpoints: {
sendMessage: {
method: 'post',
url: '/send-message/:postId',
// You can also specify strategy and other settings in here, just for this endpoint
// strategy: 'silent',
},
},
});
async function sendMessage() {
await api.sendMessage({
body: { message: 'Text' },
urlPathParams: { postId: 1 },
strategy: 'silent',
onError(error) {
console.log(error);
},
});
// Because of the strategy, if API call fails, it will never reach this point. Otherwise try/catch would need to be required.
console.log('Message sent successfully');
}
// Note that since strategy is "silent" and sendMessage should not be awaited anywhere
sendMessage();
onError
Interceptortypescript
import { createApiFetcher } from 'fetchff';
const api = createApiFetcher({
apiUrl: 'https://example.com/api',
endpoints: {
sendMessage: {
method: 'post',
url: '/send-message/:postId',
},
},
});
async function sendMessage() {
await api.sendMessage({
body: { message: 'Text' },
urlPathParams: { postId: 1 },
onError(error) {
console.log('Error', error.message);
console.log(error.response);
console.log(error.config);
},
});
console.log('Message sent successfully');
}
sendMessage();
typescript
import { createApiFetcher } from 'fetchff';
// Initialize API fetcher with endpoints
const api = createApiFetcher({
endpoints: {
getUser: { url: '/user' },
createPost: { url: '/post' },
},
apiUrl: 'https://example.com/api',
});
async function fetchUserAndCreatePost(userId: number, postData: any) {
// Fetch user data
const { data: userData } = await api.getUser({ params: { userId } });
// Create a new post with the fetched user data
return await api.createPost({
body: {
...postData,
userId: userData.id, // Use the user's ID from the response
},
});
}
// Example usage
fetchUserAndCreatePost(1, { title: 'New Post', content: 'This is a new post.' })
.then((response) => console.log('Post created:', response))
.catch((error) => console.error('Error:', error));
fetchff
is designed to seamlessly integrate with any popular frameworks like Next.js, libraries like React, Vue, React Query and SWR. It is written in pure JS so you can effortlessly manage API requests with minimal setup, and without any dependencies.
useFetcher()
hook to handle the data fetching. Since this package has everything included, you don’t really need anything more than a simple hook to utilize.api.ts
file:tsx
import { createApiFetcher } from 'fetchff';
export const api = createApiFetcher({
apiUrl: 'https://example.com/api',
strategy: 'softFail',
endpoints: {
getProfile: {
url: '/profile/:id',
},
},
});
useFetcher.ts
file:tsx
export const useFetcher = (apiFunction) => {
const [data, setData] = useState(null);
const [error] = useState(null);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
const { data, error } = await apiFunction();
if (error) {
setError(error);
} else {
setData(data);
}
setLoading(false);
};
fetchData();
}, [apiFunction]);
return { data, error, isLoading, setData };
};
tsx
export const ProfileComponent = ({ id }) => {
const {
data: profile,
error,
isLoading,
} = useFetcher(() => api.getProfile({ urlPathParams: { id } }));
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(profile)}</div>;
};
fetchff
with React Query to streamline your data fetching:tsx
import { createApiFetcher } from 'fetchff';
const api = createApiFetcher({
apiUrl: 'https://example.com/api',
endpoints: {
getProfile: {
url: '/profile/:id',
},
},
});
export const useProfile = ({ id }) => {
return useQuery(['profile', id], () =>
api.getProfile({ urlPathParams: { id } }),
);
};
fetchff
with SWR for efficient data fetching and caching.typescript
const fetchProfile = ({ id }) =>
fetchf('https://example.com/api/profile/:id', { urlPathParams: id });
export const useProfile = ({ id }) => {
const { data, error } = useSWR(['profile', id], fetchProfile);
return {
profile: data,
isLoading: !error && !data,
isError: error,
};
};
tsx
import { createApiFetcher } from 'fetchff';
import useSWR from 'swr';
const api = createApiFetcher({
apiUrl: 'https://example.com/api',
endpoints: {
getProfile: {
url: '/profile/:id',
},
},
});
export const useProfile = ({ id }) => {
const fetcher = () => api.getProfile({ urlPathParams: { id } });
const { data, error } = useSWR(['profile', id], fetcher);
return {
profile: data,
isLoading: !error && !data,
isError: error,
};
};
typescript
// src/api.ts
import { createApiFetcher } from 'fetchff';
const api = createApiFetcher({
apiUrl: 'https://example.com/api',
strategy: 'softFail',
endpoints: {
getProfile: { url: '/profile/:id' },
},
});
export default api;
typescript
// src/composables/useProfile.ts
import { ref, onMounted } from 'vue';
import api from '../api';
export function useProfile(id: number) {
const profile = ref(null);
const isLoading = ref(true);
const isError = ref(null);
const fetchProfile = async () => {
const { data, error } = await api.getProfile({ urlPathParams: { id } });
if (error) isError.value = error;
else if (data) profile.value = data;
isLoading.value = false;
};
onMounted(fetchProfile);
return { profile, isLoading, isError };
}
html
<!-- src/components/Profile.vue -->
<template>
<div>
<h1>Profile</h1>
<div v-if="isLoading">Loading...</div>
<div v-if="isError">Error: {{ isError.message }}</div>
<div v-if="profile">
<p>Name: {{ profile.name }}</p>
<p>Email: {{ profile.email }}</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useProfile } from '../composables/useProfile';
export default defineComponent({
props: { id: Number },
setup(props) {
return useProfile(props.id);
},
});
</script>
fetchff
is designed to work seamlessly with modern environments (ES2018+), some older browsers or specific edge cases might require additional support.fetchff
offers three types of builds:dist/browser/index.mjs
ES2018+
dist/browser/index.global.js
ES2018+
dist/node/index.js
Node.js 18+
fetch
API. You can use libraries like whatwg-fetch to provide a fetch implementation.AbortController
API used for aborting fetch requests. You can use the abort-controller polyfill.If you have any idea for an improvement, please file an issue. Feel free to make a PR if you are willing to collaborate on the project. Thank you :)