Data Mapping
In real-world applications, the data structures used by your frontend often differ from what your API provides or expects. Data mapping is the process of transforming data from one shape to another. Hyper-Fetch provides powerful mapping capabilities to keep your application's data models clean and decoupled from the API's implementation details.
- How to map the entire request object before it's sent using
.setRequestMapper(). - How to create type-safe
FormDatapayloads with request mapping. - How to map the raw API response to your application's data model using
.setResponseMapper(). - How to ensure end-to-end type safety with response mapping.
- How to use multiple mappers on a single cached resource for different data views.
Request Mapping
You can intercept and modify any request right before it's sent using the .setRequestMapper() method. This is
incredibly useful for tasks that need to happen for every request, such as adding authentication tokens or dynamic
headers.
When is it helpful?
- Adding an
Authorizationheader with a token from local storage. - Injecting dynamic values into headers or query parameters.
- Logging request details for debugging purposes.
const getUser = client
.createRequest<{ response: User }>()({
endpoint: "/users/:userId",
method: "GET",
})
.setRequestMapper((request) => {
// This function receives the entire request object
const token = localStorage.getItem("token");
// You can modify it, for example, by adding new headers
return request.setHeaders({
...request.headers,
Authorization: `Bearer ${token}`,
});
});
Type-Safe FormData
Creating FormData objects can be clumsy and error-prone, often sacrificing type safety. You can solve this by using
.setRequestMapper() to transform a strongly-typed object from your application into FormData right before the
request is sent. This keeps your component-level code clean and fully typed.
When is it helpful?
- Uploading files along with typed metadata.
- Ensuring that form data structures are consistent and type-checked.
- Keeping API-specific formatting logic out of your components.
interface UserProfile {
username: string;
avatar: File;
}
const updateUserProfile = client
.createRequest<{ payload: UserProfile }>()({
endpoint: "/users/profile",
method: "POST",
})
.setRequestMapper((request) => {
const data = request.data; // Type-safe UserProfile object
const formData = new FormData();
formData.append("username", data.username);
formData.append("avatar", data.avatar);
// Set the transformed FormData back on the request
return request.setPayload(formData);
});
// We can now send a strongly-typed object.
updateUserProfile.send({
payload: {
username: "Maciej",
avatar: new File([""], "avatar.jpg", { type: "image/jpeg" }),
},
});
Response Mapping
APIs often return data in a nested or complex structure that isn't ideal for direct use in your application. You can use
.setResponseMapper() to transform the raw API response into the exact shape your application needs.
When is it helpful?
- Extracting a specific data array from a paginated response object (e.g.,
response.data.users). - Flattening nested API responses into a simpler structure.
- Renaming properties to match your application's conventions (e.g.,
user_idtouserId).
// API returns { data: { users: [...] } }
interface ApiUserResponse {
data: {
users: User[];
};
}
// We want a simple User[] array
const getUsers = client
.createRequest<{ response: ApiUserResponse }>()({
endpoint: "/users",
method: "GET",
})
.setResponseMapper((response) => {
// This function receives the raw API response
// We can extract and return only the data we need
return response.data.users;
});
Type-Safety for Response Mapping
When you use .setResponseMapper(), Hyper-Fetch ensures end-to-end type safety. By defining the expected response type
in createRequest and specifying the return type of your mapper, you can guarantee that the data flowing into your
application is always correctly typed.
// 1. The raw API response type
interface ApiUserResponse {
data: {
user: {
id: number;
user_name: string;
is_active: boolean;
};
};
}
// 2. The clean data model our application uses
interface User {
id: number;
name: string;
isActive: boolean;
}
const getUser = client
.createRequest<{ response: ApiUserResponse }>()({
endpoint: "/users/:userId",
method: "GET",
})
// 3. The mapper transforms the API response to our app's model
.setResponseMapper<User>((response) => {
const user = response.data.user;
return {
id: user.id,
name: user.user_name,
isActive: user.is_active,
};
});
// 4. The `data` returned from `useFetch(getUser)` will be of type `User`
Multiple Mappers on Cached Data
A powerful feature of Hyper-Fetch is that response mapping occurs after caching. The raw, untransformed data from the API is stored in the cache. This allows you to create multiple requests for the same resource, each with a different mapper, to get different views of the same data without making extra network calls.
When is it helpful?
- A component needs just a user's name, while another needs their full profile.
- Creating computed properties from the same base data for different UI components.
- Improving performance by fetching a resource once and re-shaping it on the client for various needs.
// Base request that gets the raw user data and caches it
const getBaseUser = client.createRequest<{ response: User }>()({
endpoint: "/users/1",
});
// Mapper 1: Gets only the user's name
const getUserName = getBaseUser.setResponseMapper((user) => user.name);
// Mapper 2: Gets the user's name and formats it for a title
const getUserTitle = getBaseUser.setResponseMapper((user) => `User: ${user.name}`);
// When useFetch(getUserName) and useFetch(getUserTitle) are called,
// only one network request is made. The second call will read from
// the cache and apply its own mapper dynamically.
Centralized Mapping with SDK Configuration
When using the SDK pattern, you can centralize all your response and request mappers in a single
$configure call instead of chaining .setResponseMapper() on each request individually. This keeps your mapper
configuration in one place and ensures it's always applied.
import { createConfiguration } from "@hyper-fetch/core";
import type { ApiSchema } from "./schema";
const config = createConfiguration<ApiSchema>()({
"users.$get": (request) =>
request.setResponseMapper((response) => response.data.users),
"users.$userId.$get": (request) =>
request.setResponseMapper((response) => ({
id: response.data.user.id,
name: response.data.user.user_name,
isActive: response.data.user.is_active,
})),
});
const sdk = createSdk(client).$configure(config);
Every request accessed through the SDK will automatically have its mapper applied. See the SDK Configuration docs for more details.
You are now an expert in Hyper-Fetch data mapping!
- You can use
.setRequestMapper()to modify requests for auth, logging, or creating type-safeFormData. - You can use
.setResponseMapper()to transform API data structures into clean, application-ready models. - You can ensure end-to-end type safety by combining generics with response mappers.
- You can leverage the cache to apply multiple, different data transformations to a single resource, optimizing performance.
