Request
The Request
class is the cornerstone of Hyper Fetch's data-fetching system. It provides a powerful, type-safe way to
define, configure, and manage API requests throughout your application. By encapsulating all the details needed to
perform a request—such as endpoint, method, parameters, payload, and behavior - Request
ensures consistency,
predictability, and flexibility in how you interact with remote data sources.
With a strict and predictable data structure, Request
makes it easy to serialize, persist, and rehydrate request
states, enabling advanced features like offline support, caching, and queueing. Its design, especially when paired with
TypeScript, helps you catch errors early and build robust, maintainable data flows.
- Requests role is to create consistent API request templates.
- All requests "templates" are reusable and share the data structure.
- They behave in the same way no matter what the adapter is, to improve reusability.
- Request class holds instructions on how requests should be sent, queued, retried, or cancelled.
- Responses are typed and structured to make it easy to work with the data.
Initialization
Request should be initialized from the Client instance with createRequest
method. This passes a
shared reference to the place that manages communication in the application.
import { client } from "./client";
const getUser = client.createRequest()({
endpoint: "/users/:userId",
});
The createRequest
method currently employs a currying pattern to facilitate auto-generated TypeScript types for
endpoint strings. This is a temporary approach until
this specific TypeScript issue is resolved, which will allow for
a more direct typing mechanism.
Quick Start
Here is a quick start guide to help you get started with Requests.
Fetching
There are two main methods to send a request with Hyper Fetch - send(options)
and exec(options)
.
-
.send(options)
You can perform a request with the send(options)
method. This is the most common way to send a request with Hyper
Fetch. It triggers the request's lifecycle, including queueing, deduplication, and response handling, and returns the
server's response in a developer-friendly format.
// Simple fetch
const { data, error } = await getNotes.setQueryParams({ sort: "age" }).send();
// Multiple chained methods
const { data, error } = await getCategory.setParams({ categoryId: 2 }).send();
// Passing options
const { data, error } = await getCategory.send({ params: { categoryId: 1 }, queryParams: { sort: "createdAt" } });
-
.exec(options)
This method is very similar to send
but it ignores the built-in features - like for example deduplication or caching.
It is useful when you want to execute a request outside of the normal flow or when you want to bypass the cache
interactions - for example while using in SSR environments.
const { data, error } = await getUser.exec();
Request Options
Each request can be configured with options. We can specify things like endpoint
, method
, caching strategy
,
deduplication
, etc. This information is stored as a template for later use.
const request = client.createRequest()({
// Here are some basic options
endpoint: "/some-endpoint",
method: "POST",
deduplicate: true,
});
Name | Type | Description |
---|---|---|
abortKey |
| Key which will be used to cancel requests. Autogenerated by default. |
auth |
| Should the onAuth method get called on this request |
cache |
| Should we save the response to cache |
cacheKey |
| Key which will be used to cache requests. Autogenerated by default. |
cacheTime |
| Should we trigger garbage collection or leave data in memory |
cancelable |
| Should enable cancelable mode in the Dispatcher |
deduplicate |
| Should we deduplicate two requests made at the same time into one |
deduplicateTime |
| Time of pooling for the deduplication to be active (default 10ms) |
disableRequestInterceptors |
| Disable pre-request interceptors |
disableResponseInterceptors |
| Disable post-request interceptors |
endpoint |
| Determine the endpoint for request request |
headers |
| Custom headers for request |
method |
| Request method picked from method names handled by adapter With default adapter it is GET | POST | PATCH | PUT | DELETE |
offline |
| Do we want to store request made in offline mode for latter use when we go back online |
options |
| Additional options for your adapter, by default XHR options |
queryKey |
| Key which will be used to queue requests. Autogenerated by default. |
queued |
| Should the requests from this request be send one-by-one |
retry |
| Retry count when request is failed |
retryTime |
| Retry time delay between retries |
staleTime |
| Time for which the cache is considered up-to-date |
Payload
When you want to send some data along with the request, you can use the setPayload
method. This can be a json,
FormData or other format - depending on the adapter you use.
First we define request expecting proper type.
const postUser = client.createRequest<{
response: UserModel;
payload: UserPostDataType;
}>()({
method: "POST",
endpoint: "/users",
});
const postFile = client.createRequest<{
payload: FormData;
}>()({
method: "POST",
endpoint: "/files",
});
// Regular data
const response = await postUser.setPayload({ name: "John", age: 18 }).send();
You can also pass payload directly to the send
method.
const response = await postFile.send({ payload: data });
Payload with files
When you want to send a file, you can use the FormData
.
// Form data
const data = new FormData();
const file = new File([], "file.txt");
data.append("file", file);
const response = await postFile.setPayload(data).send();
Parameters
Parameters are the URL parts used to identify a specific resource. In HyperFetch they are defined in the endpoint part
with :
prefix. For example:
const getNote = client.createRequest()({
endpoint: "/note/:noteId", // noteId is a parameter
});
const getCategory = client.createRequest()({
endpoint: "/category/:categoryId", // categoryId is a parameter
});
const getCategoryNote = client.createRequest()({
endpoint: "/category/:categoryId/note/:noteId", // categoryId and noteId are parameters
});
When you have properly prepared requests that expect parameters, you can add parameters using the setParams
method. In
generic TypeScript, these parameters will match the endpoint parameters by using literal types and will require literal
types.
getNote.setParams({ noteId: 1 });
getCategory.setParams({ categoryId: 2 });
getCategoryNote.setParams({ categoryId: 2, noteId: 1 });
You can also pass parameters directly to the send
method.
const response = await getCategoryNote.send({ params: { categoryId: 1, noteId: 2 } });
Query parameters
Query parameters are used to filter or sort the data. In Http requests they are defined in the endpoint part with ?
prefix. For example ?search=John&sort=age
.
We can set query params with the setQueryParams
method.
const getUsers = client.createRequest<{
queryParams?: { search?: string; sort?: string };
}>()({
endpoint: "/users",
});
const response = await getUsers.setQueryParams({ search: "John" }).send();
Note that we use ?:
prefix to make query params optional. We can make it required as a whole but also particular
params from the set.
You can make query params required by specifying them without ?:
prefix.
// All params are required before request is sent
const getUsers = client.createRequest<{
queryParams: { search: string; sort: string };
}>()({
endpoint: "/users",
});
// Typescript error - sort is required
const response = await getUsers.setQueryParams({ search: "John" }).send();
// Typescript error - search is required
const response = await getUsers.setQueryParams({ sort: "age" }).send();
// Typescript error - query params are required
const response = await getUsers.send();
getUsers.setQueryParams({ search: "John", sort: "age" });
The encoding type for arrays and other options can be set up in the Client. You can also provide your own encoding logic.
getUsers.setQueryParams({ search: "John", sort: "age" });
Adapter Options
You can pass adapter options down with the request options.
const request = client.createRequest()({
endpoint: "/some-endpoint",
// Here are adapter options for the Http adapter (default one)
options: { timeout: 1000, withCredentials: true },
});
Adapter options may vary depending on the adapter you use.
Methods
The Request
class offers a suite of methods to configure and interact with a request instance. You can find a
comprehensive list of these methods and their detailed descriptions in the API reference.
Name | Description |
---|---|
| |
| |
| |
| |
| |
| |
| Map the response to the new interface |
| Map request before it gets send to the server |
| |
| |
| |
| Map data before it gets send to the server |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Read the response from cache dataIf it returns error and data at the same time, it means that latest request was failed and we show previous data from cache together with error received from actual request |
| |
| |
| |
| |
send | Method used to perform requests with usage of cache and queues |
exec | Method to use the request WITHOUT adding it to cache and queues. This mean it will make simple request without queue side effects. |
It is crucial to understand how these methods operate to ensure predictable behavior:
Most methods on a Request
instance (e.g., setParams
, setData
, setHeaders
) do not modify the original request
instance in place. Instead, they return a new, cloned instance of the request with the specified modifications
applied. The original request instance remains unchanged.
Always use the returned new instance for subsequent operations or for sending the request. This immutable approach ensures isolation between different request configurations and is fundamental to how Hyper Fetch manages request states for features like caching and queueing.
const initialRequest = getUser;
// ❌ WRONG - Original request (getUser) remains unchanged
initialRequest.setParams({ userId: 1 }); // setParams returns a new request instance
// 'initialRequest' still refers to the original getUser without params.
// Sends the original request without params, likely leading to an error.
const { data, error } = await initialRequest.send();
// ✅ CORRECT - Use the returned (cloned) instance
const originalRequest = getUser;
// Assign the new, cloned request with parameters to a new variable
const requestWithParams = originalRequest.setParams({ userId: 1 });
// Send the correctly configured request
const { data, error } = await requestWithParams.send(); // Success!
// ✅ Also correct (method chaining creates and passes clones implicitly)
const { data: chainedData, error: chainedError } = await getUser
.setParams({ userId: 1 }) // .setParams returns a clone
.send(); // .send operates on the final configured clone
Keys
Each Request
instance is assigned several unique identifiers, known as keys. These keys are essential for internal
mechanisms—such as caching, queueing, request cancellation, deduplication, effect management and many more. Think of
them as "fingerprints" that help Hyper Fetch track, manage, and use for assigning side-effects to requests.
Keys are generated automatically based on the request's endpoint, method, and parameters. Currently there are two types of the generation mechanisms - simple and complex keys. Complex keys include query params in the generation - simple keys include only endpoint and method.
queryKey
General identifier for the request, used for query management (e.g., refetching, optimistic updates, invalidation).
const key = request.queryKey;
Set custom key with setQueryKey
method.
const customRequest = request.setQueryKey("custom-key");
cacheKey
Determines how the response is stored and retrieved from the cache. Ensures correct data is cached and served.
const key = request.cacheKey;
Set custom key with setCacheKey
method.
const customRequest = request.setCacheKey("custom-key");
abortKey
Identifies and manages ongoing requests for cancellation (e.g., aborting in-flight requests).
const key = request.abortKey;
Set custom key with setAbortKey
method.
const customRequest = request.setAbortKey("custom-key");
Generation mechanism
By default, these keys are auto-generated based on the request's endpoint, method, and parameters. This ensures that each unique request configuration is tracked separately. Your can override this mechanism by providing your own implementation.
import { client } from "./api";
client.setCacheKeyMapper((request) => {
if (request.requestOptions.endpoint === "/users/:userId") {
return `CUSTOM_CACHE_KEY_${request.params?.userId || "unknown"}`;
}
});
Why override keys?
In most cases, the default keys are sufficient. However, you may want to override them for advanced scenarios, such as:
- Custom cache strategies: Grouping multiple endpoints under a single cache key.
- Manual request deduplication: Treating different requests as identical for deduplication or cancellation.
- Abort groups: Being able to abort multiple requests at once.
Overriding keys
You can override any key using the corresponding method:
const customRequest = request.setQueryKey("custom-key");
const customCacheRequest = request.setCacheKey("my-cache-key");
const abortableRequest = request.setAbortKey("my-abort-key");
const effectLinkedRequest = request.setEffectKey("my-effect-key");
Note: Overriding keys is an advanced feature. Make sure you understand the implications for caching, deduplication, and request management.
Features
The Request
class is packed with features to streamline your data fetching and management. Here are some key
capabilities:
-
Cancellation
Cancellation allows you to abort an in-progress request before it completes. This is useful in scenarios where the result of a request is no longer needed, such as when a user navigates away from a page or changes a filter before the previous request finishes. Aborting a request helps prevent unnecessary processing and side effects from outdated responses.
-
Manual cancellation
Triggered by the .abort()
request method, allow us to stop the request execution.
-
Automatic cancellation
It is automatically executed when we send two identical requests at the same time. Older request will be cancelled and the new one will be sent.
-
Queueing
Queueing ensures that requests are sent one after another, rather than in parallel. When enabled, each request waits for the previous one to finish before starting. This is important for operations that must be performed in order, or when interacting with APIs that require serialized access.
-
Offline Support
Offline support allows requests to be stored when the application is offline. These requests are automatically retried once the network connection is restored. This feature is useful for applications that need to function reliably in environments with intermittent connectivity.
-
Deduplication
Deduplication prevents multiple identical requests from being sent at the same time. If a request is already in progress, additional requests with the same parameters will reuse the ongoing request and share its response. This reduces redundant network traffic and ensures consistent results.
-
Authentication
Authentication integrates the request with the client's authentication mechanism. When enabled, authentication tokens or credentials are automatically included with the request. This is necessary for accessing protected resources or APIs that require user identity.
// Add the onAuth interceptor
client.onAuth(({ request }) => {
// Modify request - by adding the auth token to the headers
request.setHeaders({
Authorization: `Bearer ${auth.token}`,
});
return request;
});
// Authenticated request (uses Client's auth setup)
const authRequest = client.createRequest()({ endpoint: "/profile", auth: true });
// it will authenticate the request
authRequest.send();
-
Mapping
Response mapping allows you to transform the request payload before sending it, or the response data after receiving it. This is useful for adapting data formats between your application and external APIs, or for preprocessing data for validation or display.
// Map response data to a new structure
const { data: mappedResponse } = await client
.createRequest<{ response: { name: string } }>()({ endpoint: "/user" })
.setResponseMapper((res) => ({ ...res, data: { displayName: res.data.name } }))
.send();
console.log(mappedResponse); // { displayName: "John" }
Request mapping provides hooks for custom transformations of payloads, requests, and responses. This allows for complex data processing scenarios, such as chaining multiple transformations, integrating with third-party services, or adapting to evolving API contracts.
// Advanced mapping: transform payload before sending
const request = client
.createRequest<{ payload: { name: string } }>()({ endpoint: "/advanced" })
.setPayloadMapper((payload) => ({ ...payload, name: payload.name.toUpperCase() }));
// Will send the request with the payload { name: "JOHN" }
const mappedRequest = request.setPayload({ name: "john" }).send();
-
Retries
Retries automatically resend a request if it fails due to network errors or server issues. You can configure the number of retry attempts and the delay between them. This feature helps improve reliability in the face of transient errors.
// Retry failed requests automatically
const retryRequest = client.createRequest()({ endpoint: "/unstable", retry: 2, retryTime: 1000 });
retryRequest.send(); // Will retry up to 2 times on failure
-
Error handling
Error handling provides mechanisms to detect and respond to errors that occur during the request lifecycle. You can define callbacks for different error types, such as network failures, validation errors, or server responses, allowing your application to handle failures appropriately.
// Handle errors with onError callback
const errorRequest = client.createRequest()({ endpoint: "/fail" });
// Will return error without throwing it
const { error } = await errorRequest.send();
// You can also handle the error with callback
await errorRequest.send({
onError: ({ response }) => {
console.error("Request failed:", response.error);
},
});
-
Validation
Validation enables you to check the request data before it is sent, and the response data after it is received. This helps ensure that only valid data is processed by your application, and that errors are caught early in the data flow.
// Validate request data before sending
const validatedRequest = client
.createRequest<{ payload: { age: number } }>()({ endpoint: "/validate" })
.setRequestMapper((req) => {
if (req.payload.age < 18) throw new Error("Must be 18+");
return req;
});
const { error } = await validatedRequest.setPayload({ age: 16 }).send();
console.log(error); // Error: Must be 18+
Lifecycle listeners
The send()
method accepts an options object where you can define lifecycle callback functions. These callbacks allow
you to hook into various stages of the request's lifecycle—such as onBeforeSent
(when the request finishes, regardless
of outcome), onSuccess
, onError
, onStart
, onProgress
, onAbort
, and onOfflineError
—to perform actions based
on the request's progress and outcome.
await someRequest.send({
onBeforeSent: ({ response, requestId }) => {
console.log(`Request ${requestId} has settled. Final response:`, response);
// Called right before the request is sent—useful for quickly accessing the requestId for tracking or logging
},
onStart: ({ requestId, request }) => {
console.log(`Request ${requestId} has started.`);
// Called when the request is started—useful for showing a loading indicator
},
onUploadProgress: ({ progress, timeLeft, sizeLeft, total, loaded, startTimestamp, request, requestId }) => {
console.log(`Request ${requestId} upload progress:`, progress);
// Called when the upload progress is updated—useful for showing a progress bar
},
onDownloadProgress: ({ progress, timeLeft, sizeLeft, total, loaded, startTimestamp, request, requestId }) => {
console.log(`Request ${requestId} download progress:`, progress);
// Called when the download progress is updated—useful for showing a progress bar
},
onResponse: ({ response, requestId }) => {
console.log(`Request ${requestId} finished with data:`, response.data);
// Called when the request succeeds—useful for processing successful data
},
onRemove: ({ request, requestId }) => {
console.log(`Request ${requestId} removed.`);
// Called when the request is removed—useful for cleaning up
},
});
TypeScript
When creating a request with client.createRequest
, you can specify up to four generic types to ensure type safety and
enhance the developer experience throughout the request lifecycle:
response
: The expected type of the successful response data from the server.payload
: The type of the data to be sent in the request body.error
: The type for errors specific to this request, often used for local validation or business logic errors, complementing the global error type defined on theClient
.queryParams
: The expected structure and types for the request's query parameters.
const request = client.createRequest<{
response: { name: string };
payload: { age: number };
error: { message: string };
queryParams: { search: string };
}>()({ endpoint: "/user" });
const { data, error } = await request.send();
console.log(data); // { name: "John" }
console.log(error); // undefined
To ensure type safety, we recommend installing the eslint-plugin-hyper-fetch plugin. It will help you catch type errors early and build robust, maintainable data flows.
Learn how to install and configure the plugin in the Eslint plugin guide.
Generics
Starting from Hyper Fetch v7.0.0
, all the types for the Client
class and the createRequest
method are passed as
objects. This way, user can specify only what they want, in any order, in very a readable way.
const client = new Client<{ error: ErrorType }>({ url });
const getUsers = client.createRequest<{ response: ResponseType; queryParams: QueryParamsType }>()({
endpoint: "/users",
});
We firmly believe that it is more readable approach. Yet, it comes at a cost - currently, TypeScript does not handle well the autosuggestions and type inference for the extended object-like generics (https://github.com/microsoft/TypeScript/issues/28662). Fear not - the types are working correctly and we found a way to make it work with our own eslint plugin.
For example, when writing a request, e.g.:
const postUser = client.createRequest<{ invalidGeneric: string }>()({ endpoint: "/" });
TypeScript will not throw an error if you use an invalid key like invalidGeneric
that isn't part of our API. To catch
these issues early, we recommend using our custom ESLint plugin. This approach combines the benefits of type safety and
readability, while ensuring your code uses the correct API syntax.