Platform
    • RTK Query
    • Mock Server (MSW)
    • Data Converters
    • OpenAPI Codegen
Projects
  • API Connections & Tools
  1. Documentation
  2. Data Converters

Конвертеры данных (DTO -> Model)

Руководство по разделению контрактов API и внутренним моделям данных приложения с помощью маппингов (DTO -> Domain Model).

Архитектура маппинга данных

Преимущества: Конвертеры изолируют UI от изменений на бэкенде, позволяют форматировать данные (даты, перечисления) и гарантируют типобезопасность на уровне слоев.

Структура файлов конвертеров сущности

Соглашения о наименовании

  • Конвертеры:

    Располагаются в папке converters. Файлы называются по шаблону [entity-name].converters.ts. Используют типы из соседней папки для трансформации данных.

  • Типы и Интерфейсы:

    Располагаются в папке types. Обязательно разделение на *-backend.interface.ts (сырые DTO как T*Backend) и *.interface.ts (модели UI как I*). Все типы экспортируются через index.ts.

01Маппинг в Frontend (Domain Model)

Преобразование сырых данных бэкенда (snake_case, строки вместо дат) в camelCase и типизированные объекты для UI. Имена операций: mapEntityToFrontend / mapEntitiesToFrontend — литерал mapToFrontend запрещён.

converters/booking-order.converters.ts
1import { formatDate } from "@/shared/lib/utils"; 2import type { IPaginationResponse } from "@/shared/types"; 3import type { TEntityBackend, IEntity, TEntitiesBackend } from "../types"; 4 5export const mapEntityToFrontend = ( 6 data: TEntityBackend 7): IEntity => ({ 8 id: data.id, 9 fullName: data.full_name, 10 dateCreated: formatDate(data.created_at), 11 status: data.status_code 12}); 13 14/** Backend list envelope → unified { data, total } */ 15export const mapEntitiesToFrontend = ( 16 response: TEntitiesBackend 17): IPaginationResponse<IEntity> => ({ 18 data: response.items.map(mapEntityToFrontend), 19 total: response.total_count 20});

02Маппинг в Backend (DTO)

Подготовка данных перед отправкой. Используется при создании или обновлении сущностей для возврата к формату DTO, ожидаемому API.

converters/booking-order.converters.ts
1import type { TEntityBackend, IEntity } from "../types"; 2 3export const mapEntityToBackend = ( 4 data: Partial<IEntity> 5): Partial<TEntityBackend> => ({ 6 id: data.id, 7 full_name: data.fullName, 8 created_at: data.dateCreated, 9 status_code: data.status 10});

03Конвертация фильтров

Особый случай маппинга стейта поисковых фильтров в query-параметры. Позволяет удобно объединять массивы, обрабатывать пустые значения и специфичные форматы API.

converters/booking-order.converters.ts
1import type { IPaginationRequest } from "@/shared/types"; 2import type { TEntitiesQueryBackend } from "../types"; 3 4/** IPaginationRequest → API-specific query fields (names stay here) */ 5export const mapEntitiesQueryToBackend = ( 6 filters: IPaginationRequest 7): TEntitiesQueryBackend => ({ 8 page: filters.page, 9 elements_on_page: filters.limit, 10 search: filters.search || undefined, 11 status_code: filters.status 12});

04Интеграция с RTK Query

Конвертеры внедряются прямо в описание эндпоинтов: transformResponse для получения данных и query (params/body) для отправки. Импорт converters в pages/widgets/features запрещён — UI уже получает frontend-модели.

api/[entity-name].service.ts
1import { baseApi } from "@/shared/api/backend"; 2import type { IPaginationRequest, IPaginationResponse } from "@/shared/types"; 3import { 4 mapEntitiesQueryToBackend, 5 mapEntitiesToFrontend, 6 mapEntityToBackend 7} from "../converters"; 8import type { TEntitiesBackend, IEntity } from "../types"; 9 10/** Only place converters are imported — never in pages/widgets/features/ui */ 11export const entityApi = baseApi.injectEndpoints({ 12 endpoints: (builder) => ({ 13 getEntities: builder.query< 14 IPaginationResponse<IEntity>, 15 IPaginationRequest 16 >({ 17 query: (filters) => ({ 18 url: "/entity/list", 19 params: mapEntitiesQueryToBackend(filters) 20 }), 21 transformResponse: (response: TEntitiesBackend) => 22 mapEntitiesToFrontend(response) 23 }), 24 updateEntity: builder.mutation<void, IEntity>({ 25 query: (body) => ({ 26 url: "/entity/update", 27 method: "POST", 28 body: mapEntityToBackend(body) 29 }) 30 }) 31 }) 32});

05ESLint: converters не выходят за entity

Фиксируем через no-restricted-imports (блок @/entities/*/converters) и eslint-plugin-boundaries (импорт converters только из api/ той же сущности). Относительный ../converters из api/ разрешён.
eslint.config.js (converters isolation)
1import boundaries from "eslint-plugin-boundaries"; 2 3export default [ 4 { 5 plugins: { boundaries }, 6 settings: { 7 "boundaries/elements": [ 8 { 9 type: "entity-converters", 10 pattern: "src/entities/*/converters/**", 11 capture: ["entity"] 12 }, 13 { 14 type: "entity-api", 15 pattern: "src/entities/*/api/**", 16 capture: ["entity"] 17 }, 18 { type: "pages", pattern: "src/pages/**" }, 19 { type: "widgets", pattern: "src/widgets/**" }, 20 { type: "features", pattern: "src/features/**" }, 21 { type: "entities", pattern: "src/entities/**" }, 22 { type: "shared", pattern: "src/shared/**" } 23 ], 24 "boundaries/ignore": ["**/*.{test,spec}.{ts,tsx}"] 25 }, 26 rules: { 27 // 1) Block public/alias imports of converters (api uses relative ../converters) 28 "no-restricted-imports": [ 29 "error", 30 { 31 patterns: [ 32 { 33 group: [ 34 "**/entities/*/converters", 35 "**/entities/*/converters/**", 36 "@/entities/*/converters", 37 "@/entities/*/converters/**" 38 ], 39 message: 40 "Converters are private to the entity. Import only from entities/<name>/api via relative '../converters'. UI must use frontend models from RTK hooks." 41 } 42 ] 43 } 44 ], 45 // 2) Only same-entity api may depend on converters segment 46 "boundaries/element-types": [ 47 "error", 48 { 49 default: "disallow", 50 rules: [ 51 { 52 from: { type: "entity-api", capture: ["entity"] }, 53 allow: [ 54 { type: "entity-converters", capture: ["entity"] }, 55 "entities", 56 "shared" 57 ] 58 }, 59 { 60 from: "entity-converters", 61 allow: ["entities", "shared"] 62 }, 63 { 64 from: "pages", 65 allow: ["widgets", "features", "entities", "shared"] 66 }, 67 { 68 from: "widgets", 69 allow: ["features", "entities", "shared"] 70 }, 71 { 72 from: "features", 73 allow: ["entities", "shared", "features"] 74 }, 75 { 76 from: "entities", 77 allow: ["entities", "shared"] 78 }, 79 { 80 from: "shared", 81 allow: ["shared", "entities"] 82 } 83 ] 84 } 85 ], 86 "boundaries/no-private": "error" 87 } 88 } 89];