6.7 KiB
Data Layer
Table of Contents
Purpose
The data layer is responsible for all data access and data transformation in the application.
It is the only layer that knows:
- how to call APIs
- how to map API responses (DTOs) to application entities
- how to handle caching, queries, and mutations
Higher layers (forms, widgets, pages) do not interact with APIs directly.
Instead, they consume hooks provided by the data layer.
Responsibilities
The data/ layer is responsible for:
- performing API calls
- defining DTO types (Data Transfer Objects)
- converting DTOs → entities using mappers
- providing React Query hooks
- handling side effects (requests, caching, errors)
- defining API-related types
Data Layer Organization Approaches
data/
| --- user/
| | --- api.ts # user-related API functions (getUsers, createUser, etc.)
| | --- hooks.ts # React Query hooks (useUsersQuery, useCreateUserMutation)
| | --- dto.ts # API DTO types for user
| | --- mapper.ts # map user DTO → entity
| | --- types.ts # API types/interfaces
|
| --- pagination/
| | --- dto.ts
| | --- mapper.ts
| | --- types.ts
There are two main ways to structure the data layer:
- By Module — grouped by type of file (API, hooks, DTOs, mappers, types)
- By Entity — grouped by business feature/entity (user, product, message)
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| By Module | Small apps, simple data layers | Easy to find all API/hooks/DTOs in one place | Files for one feature scattered; hard to scale |
| By Entity | Medium/large apps, scalable CRM | Encapsulated, modular, easier to maintain | Harder to see all APIs/hooks globally |
By Module
data/
| --- api/ # API request functions
| | --- userAPI.ts
| | --- productAPI.ts
| | --- messageAPI.ts
|
| --- hooks/ # React Query hooks or other data hooks
| | --- useUserQuery.ts
| | --- useCreateProductMutation.ts
| | --- useMessages.ts
|
| --- dto/ # API DTO types
| | --- userDTO.ts
| | --- productDTO.ts
| | --- messageDTO.ts
|
├ --- mappers/ # DTO → entity mappers
| | --- userMapper.ts
| | --- productMapper.ts
|
| --- types/ # API types/interfaces
By Entity
data/
| --- user/
| | --- api.ts # user-related API functions (getUsers, createUser, etc.)
| | --- hooks.ts # React Query hooks (useUsersQuery, useCreateUserMutation)
| | --- dto.ts # API DTO types for user
| | --- mapper.ts # map user DTO → entity
| | --- types.ts # API types/interfaces (optional)
|
| --- product/
| | --- api.ts
| | --- hooks.ts
| | --- dto.ts
| | --- mapper.ts
| | --- types.ts
Data Flow
1. DTO Types
export type UserDTO = {
id: number;
full_name: string;
email_address: string;
};
export type CreateUserDTO = {
full_name: string;
email_address: string;
};
2. API Layer
API functions or class methods perform HTTP requests. API always returns DTO objects.
export class UserApi {
private http: HttpClient;
async getDetailed(id: number): Promise<UserDTO> {
return this.http.get(`/users/${id}`);
}
async getList(): Promise<UserDTO[]> {
return this.http.get(`/users`);
}
async create(dto: CreateUserDTO): Promise<UserDTO> {
return this.http.post(`/users`, dto);
}
}
3. Mapper
Mappers convert DTO to Entity
import { User } from "@/entities/user";
export class UserMapper {
toEntity(dto: UserDTO): User {
return {
id: dto.id,
name: dto.full_name,
email: dto.email_address,
};
}
toEntities(dtos: UserDTO[]): User[] {
return dtos.map((dto) => this.toEntity(dto));
}
toPaginated(dto: PaginatedDataDTO<UserDTO[]>): PaginatedResult<User[]> => {
return {
meta: this.paginationMetaMapper.toMeta(dto.meta),
data: this.toEntities(dto.data),
};
};
}
Mapper Testing
Mappers are part of the data layer contract and should have unit tests.
Mapper tests are required when a mapper:
- renames fields from API DTOs to app entities
- normalizes values, paths, dates, sizes, statuses, enums, or defaults
- maps nested DTOs through other mappers
- maps arrays, paginated data, or optional API fields
- protects the app from backend response shape changes
These tests should focus only on transformation behavior. Do not mock React, render components, or call real APIs in mapper tests.
Recommended test coverage:
toEntitymaps a representative DTO to the expected entitytoEntitiesmaps arrays consistently- optional or missing DTO fields use the correct defaults
- nested mappers preserve the expected entity shape
- enum/status casts are covered with expected values
For apps that keep tests outside production source, use a test folder at the app root:
apps/admin/
├── src/
│ └── core/FileManager/data/mappers/entryMapper.ts
└── tests/
└── unit/core/FileManager/data/mappers/entryMapper.test.ts
4. Query Hooks
Hooks combine API + mapper + React Query.
export const useUserDetailed = (id: number) => {
const api = new UserApi(http);
const mapper = new UserMapper();
return useQuery({
queryKey: ["user", id],
queryFn: async () => {
const dto = await api.getDetailed(id);
return mapper.toEntity(dto);
},
});
};
Pagination
Pagination is not entity-specific, so it should live in a shared data module.
export type PaginatedMetaDTO = {
total: number;
perPage: number;
currentPage: number;
lastPage: number;
};
export type PaginatedDTO<T> = {
data: T;
meta: PaginatedMetaDTO;
};
export class PaginationMapper {
toMeta(dto: PaginatedMetaDTO) {
return {
total: dto.total,
perPage: dto.perPage,
currentPage: dto.currentPage,
lastPage: dto.lastPage,
};
}
}