Files
admin/docs/apps-architecture/react-frontend/layers/data.md
T
2026-06-25 15:53:49 +03:00

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:

  • 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:

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,
    };
  }
}