Files

150 lines
4.1 KiB
Markdown

# Data Layer
The `data` layer owns all data access and transformation: API calls, DTO types, DTO → entity mappers, React Query hooks, caching and errors. Higher layers (`forms`, `widgets`, `pages`) never call APIs directly — they consume data-layer hooks.
## Organization
Two ways to structure the layer:
| Approach | Layout | Best for | Trade-off |
| --------- | ------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------- |
| By Module | `data/api/`, `data/hooks/`, `data/dto/`, `data/mappers/` | Small apps, simple data layers | One feature's files scattered; hard to scale |
| By Entity | `data/user/`, `data/product/` — each with `api.ts`, `hooks.ts`, `dto.ts`, `mapper.ts`, `types.ts` | Medium/large apps | Harder to see all APIs/hooks globally |
```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)
```
## 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 and always return DTOs.
```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 need unit tests whenever they rename fields, normalize values (paths, dates, statuses, enums, defaults), map nested DTOs, arrays, paginated data, or optional fields. Cover: `toEntity` on a representative DTO, `toEntities` on arrays, defaults for missing fields, nested mapper shapes, enum/status casts. Test only transformation — no React, no rendering, no real API calls.
Keep tests at the app root, mirroring the source path:
```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 — keep it 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,
};
}
}
```