Guide on decoupling API contracts and internal application data models using mappings (DTO -> Domain Model).
Benefits: Converters isolate the UI from backend changes, allow data formatting (dates, enums), and guarantee layer-level type safety.
Located in the converters/ folder. Files are named following the [entity-name].converters.ts pattern. They use types from the sibling folder for data transformation.
Located in the types folder. Must be divided into *.backend.interface.ts (raw DTOs) and *.interface.ts (UI models). All types are exported via index.ts.
Transforming raw backend data (snake_case, strings instead of dates) into camelCase and typed objects for the UI. mapToFrontend is the most critical stage.
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});Preparing data before sending. Used when creating or updating entities to return to the DTO format expected by the 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});A special case of mapping search filter state into query parameters. Allows convenient array joining, handling empty values, and API-specific formats.
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});Converters are injected directly into endpoint definitions: transformResponse for data retrieval and query (params/body) for data submission.
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});