261 lines
6.7 KiB
Markdown
261 lines
6.7 KiB
Markdown
# Data Layer
|
|
|
|
## Table of Contents
|
|
|
|
- [Data Layer](#data-layer)
|
|
- [Purpose](#purpose)
|
|
- [Responsibilities](#responsibilities)
|
|
- [Data Layer Organization Approaches](#data-layer-organization-approaches)
|
|
- [By Module](#by-module)
|
|
- [By Entity](#by-entity)
|
|
- [Data Flow](#data-flow)
|
|
- [DTO Types](#1-dto-types)
|
|
- [API Layer](#2-api-layer)
|
|
- [Mapper](#3-mapper)
|
|
- [Mapper Testing](#mapper-testing)
|
|
- [Query Hooks](#4-query-hooks)
|
|
- [Pagination](#pagination)
|
|
|
|
## 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
|
|
|
|
```plaintext
|
|
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
|
|
|
|
```plaintext
|
|
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
|
|
|
|
```plaintext
|
|
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
|
|
|
|
```ts
|
|
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.
|
|
|
|
```ts
|
|
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`
|
|
|
|
```ts
|
|
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:
|
|
|
|
- `toEntity` maps a representative DTO to the expected entity
|
|
- `toEntities` maps 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:
|
|
|
|
```plaintext
|
|
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.
|
|
|
|
```ts
|
|
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.
|
|
|
|
```ts
|
|
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,
|
|
};
|
|
}
|
|
}
|
|
```
|