Руководство по разделению контрактов API и внутренним моделям данных приложения с помощью маппингов (DTO -> Domain Model).
Преимущества: Конвертеры изолируют UI от изменений на бэкенде, позволяют форматировать данные (даты, перечисления) и гарантируют типобезопасность на уровне слоев.
Располагаются в папке converters. Файлы называются по шаблону [entity-name].converters.ts. Используют типы из соседней папки для трансформации данных.
Располагаются в папке types. Обязательно разделение на *.backend.interface.ts (сырые DTO) и *.interface.ts (модели для UI). Все типы экспортируются через index.ts.
Преобразование сырых данных бэкенда (snake_case, строки вместо дат) в camelCase и типизированные объекты для UI. mapToFrontend — самый важный этап.
1import { formatDate } from "@/shared/lib/utils";
2import { type IEntityBackend, type IEntity } from "../types";
3
4export const mapEntityToFrontend = (
5 data: IEntityBackend
6): IEntity => ({
7 id: data.id,
8 fullName: data.full_name,
9 dateCreated: formatDate(data.created_at),
10 status: data.status_code
11});Подготовка данных перед отправкой. Используется при создании или обновлении сущностей для возврата к формату DTO, ожидаемому API.
1import {
2 type IEntityBackend,
3 type IEntity
4} from "../types";
5
6export const mapEntityToBackend = (
7 data: Partial<IEntity>
8): Partial<IEntityBackend> => ({
9 id: data.id,
10 full_name: data.fullName,
11 created_at: data.dateCreated,
12 status_code: data.status
13});Особый случай маппинга стейта поисковых фильтров в query-параметры. Позволяет удобно объединять массивы, обрабатывать пустые значения и специфичные форматы API.
1import { type IEntityFilters } from "../types";
2
3export const mapEntityFiltersToBackend = (
4 filters: IEntityFilters
5) => ({
6 page: filters.page,
7 limit: filters.limit,
8 search: filters.search || undefined,
9 status: filters.status.length > 0
10 ? filters.status.join(",")
11 : undefined
12});Конвертеры внедряются прямо в описание эндпоинтов: transformResponse для получения данных и query (params/body) для отправки.
1import { authApi } from "@/entities/auth/api/auth.api";
2import {
3 mapEntityFiltersToBackend,
4 mapEntityPaginatedToFrontend,
5 mapEntityToBackend
6} from "../converters";
7
8export const entityApi = authApi.injectEndpoints({
9 endpoints: (builder) => ({
10 getEntities: builder.query<TResponse, TFilters>({
11 query: (filters) => ({
12 url: "/entity/list",
13 params: mapEntityFiltersToBackend(filters)
14 }),
15 transformResponse: (response: TResponseBackend) =>
16 mapEntityPaginatedToFrontend(response)
17 }),
18 updateEntity: builder.mutation<void, TEntity>({
19 query: (body) => ({
20 url: "/entity/update",
21 method: "POST",
22 body: mapEntityToBackend(body)
23 })
24 })
25 })
26});