Initial commit
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
# 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user