# 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 { return this.http.get(`/users/${id}`); } async getList(): Promise { return this.http.get(`/users`); } async create(dto: CreateUserDTO): Promise { 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): PaginatedResult => { 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 = { data: T; meta: PaginatedMetaDTO; }; export class PaginationMapper { toMeta(dto: PaginatedMetaDTO) { return { total: dto.total, perPage: dto.perPage, currentPage: dto.currentPage, lastPage: dto.lastPage, }; } } ```