Data Transformers
Purpose: Separate data transformation logic from API call logic, enabling clean DTO ↔ Entity mapping with type safety.
Location:
packages/core-api/src/data-services/base-data.transformer.ts
Why Data Transformers?
In enterprise applications, the shape of data returned by the API (DTOs) often differs from the shape used in the frontend (Domain Entities). Common differences include:
| API (DTO) | Frontend (Entity) |
|---|---|
snake_case field names | camelCase field names |
| Deeply nested structures | Flattened/normalized shapes |
| Raw ISO date strings | Parsed Date objects |
| No computed fields | Derived/computed properties |
| Backend-specific enums | Frontend-friendly enums |
Without transformers, this mapping logic leaks into components, hooks, and services — violating the Single Responsibility Principle and making the codebase harder to test and maintain.
Benefits
- Separation of Concerns — Transformation logic lives in one place, not scattered across components
- Testability — Transformers are pure functions, trivially unit-testable
- Reusability — Same transformer can be used across multiple services or contexts
- Type Safety — Two generic parameters (
TEntity,TDTO) enforce correct mapping at compile time - Backward Compatible — Transformers are optional; existing services work unchanged
Architecture
Data flows:
- API → Frontend: Response DTO →
transformToEntity()→ Domain Entity - Frontend → API: Domain Entity →
transformToDTO()→ Request DTO
Quick Start
1. Define Your Types
// Domain Entity (what your UI uses)
interface BookingEntity extends BaseEntity {
bookingCode: string;
customerName: string;
checkInDate: string;
}
// API DTO (what the backend returns)
interface BookingDTO {
id?: string;
booking_code: string;
customer_name: string;
check_in_date: string;
}2. Create a Transformer
import { BaseDataTransformer } from '@repo/core-api/data-services';
class BookingTransformer extends BaseDataTransformer<BookingEntity, BookingDTO> {
transformToEntity(dto: BookingDTO): BookingEntity {
return {
id: dto.id,
bookingCode: dto.booking_code,
customerName: dto.customer_name,
checkInDate: dto.check_in_date,
};
}
transformToDTO(entity: BookingEntity): BookingDTO {
return {
id: entity.id,
booking_code: entity.bookingCode,
customer_name: entity.customerName,
check_in_date: entity.checkInDate,
};
}
}3. Inject into Data Services
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
const bookingServices = new CommonRemoteDataServices<BookingEntity, BookingDTO>(
apiClient,
{
apiUrl: '/bookings',
moduleKey: 'BOOKING',
transformer: new BookingTransformer(),
},
);
// Now all CRUD methods automatically transform:
const { data } = await bookingServices.getOne('42');
// data is BookingEntity (camelCase) ✓
await bookingServices.create({ bookingCode: 'BK001', customerName: 'Alice', ... });
// Payload is sent as { booking_code: 'BK001', customer_name: 'Alice', ... } ✓Interface Reference
IDataTransformer<TEntity, TDTO>
The minimal contract for bidirectional data transformation.
interface IDataTransformer<TEntity, TDTO> {
transformToEntity(dto: TDTO): TEntity;
transformToDTO(entity: TEntity): TDTO;
// Optional operation-specific hooks
transformGetOneResponse?(dto: TDTO): TEntity;
transformGetManyResponse?(dtos: TDTO[]): TEntity[];
transformCreatePayload?(entity: Partial<TEntity>): Partial<TDTO>;
transformEditPayload?(entity: Partial<TEntity>): Partial<TDTO>;
}BaseDataTransformer<TEntity, TDTO>
Abstract class implementing IDataTransformer with sensible defaults.
| Method | Default Behavior | Override When |
|---|---|---|
transformToEntity | Identity cast (passthrough) | Always — this is the core mapping |
transformToDTO | Identity cast (passthrough) | Always — this is the core mapping |
transformGetOneResponse | Delegates to transformToEntity | getOne needs computed/derived fields |
transformGetManyResponse | Maps each item via transformToEntity | List responses need bulk transformations |
transformCreatePayload | Delegates to transformToDTO | Create payloads need special handling (e.g., strip IDs) |
transformEditPayload | Delegates to transformToDTO | Edit payloads differ from create |
Integration with BaseRemoteDataServices
When a transformer is injected via DataServicesConfig.transformer, the base service methods automatically apply transformations:
| Service Method | Transformer Hook Used | Direction |
|---|---|---|
getOne() | transformGetOneResponse() | Response → Entity |
getMany() | transformGetManyResponse() | Response → Entity |
create() | transformCreatePayload() | Entity → DTO |
edit() | transformEditPayload() | Entity → DTO |
delete() | None (no data transformation) | — |
customRequest() | None (manual transformation) | — |
Important: If no transformer is injected, all methods behave exactly as before — data passes through unchanged. This ensures 100% backward compatibility.
Advanced: Extending Transformers
For domain-specific features that go beyond standard CRUD, you can extend both the transformer and the data service.
Extended Transformer
// advanced-booking.transformer.ts
import { BookingTransformer } from './booking.transformer';
interface AvailabilityChartRawData {
dates: Array<{
date_iso: string;
available_rooms: number;
occupancy_rate: number;
}>;
}
interface AvailabilityChartData {
dataPoints: Array<{
label: string;
availableRooms: number;
isHighDemand: boolean;
}>;
}
class AdvancedBookingTransformer extends BookingTransformer {
// All standard CRUD mappings are inherited ✓
// Add custom transformation for non-CRUD data
transformAvailabilityChart(rawData: AvailabilityChartRawData): AvailabilityChartData {
return {
dataPoints: rawData.dates.map((item) => ({
label: new Date(item.date_iso).toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
}),
availableRooms: item.available_rooms,
isHighDemand: item.occupancy_rate > 80,
})),
};
}
}Extended Data Service
// advanced-booking.data-services.ts
import { BaseRemoteDataServices } from '@repo/core-api/data-services';
class AdvancedBookingDataServices extends BaseRemoteDataServices<BookingEntity, BookingDTO> {
private readonly advancedTransformer: AdvancedBookingTransformer;
constructor() {
const transformer = new AdvancedBookingTransformer();
super(apiClient, {
apiUrl: '/bookings',
moduleKey: 'BOOKING',
transformer,
});
this.advancedTransformer = transformer;
}
// Custom method using the extended transformer
async getAvailabilityChart(params: {
startDate: string;
endDate: string;
}): Promise<ApiResponse<AvailabilityChartData>> {
const response = await this.customRequest<AvailabilityChartRawData>({
url: '/bookings/availability-chart',
method: 'GET',
params: { start_date: params.startDate, end_date: params.endDate },
});
return {
data: this.advancedTransformer.transformAvailabilityChart(response.data),
status: response.status,
};
}
}
export const advancedBookingServices = new AdvancedBookingDataServices();Migration Guide
Adding transformers to existing services requires zero breaking changes:
Step 1: Create a Transformer
class MyTransformer extends BaseDataTransformer<MyEntity, MyDTO> {
transformToEntity(dto: MyDTO): MyEntity {
/* ... */
}
transformToDTO(entity: MyEntity): MyDTO {
/* ... */
}
}Step 2: Add a Second Generic Parameter
- const services = new CommonRemoteDataServices<MyEntity>(apiClient, {
+ const services = new CommonRemoteDataServices<MyEntity, MyDTO>(apiClient, {
apiUrl: '/my-endpoint',
+ transformer: new MyTransformer(),
});Step 3: (Optional) Override Operation-Specific Hooks
class MyTransformer extends BaseDataTransformer<MyEntity, MyDTO> {
transformToEntity(dto: MyDTO): MyEntity {
/* ... */
}
transformToDTO(entity: MyEntity): MyDTO {
/* ... */
}
// Only override if getOne needs special handling
override transformGetOneResponse(dto: MyDTO): MyEntity {
const entity = this.transformToEntity(dto);
return { ...entity, computedField: derive(dto) };
}
}Existing services without transformers are completely unaffected. The
TDTOgeneric defaults toTEntity, and thetransformerconfig property defaults toundefined.
Sample Implementation
A full working example is available in the showcase booking feature:
| File | Description |
|---|---|
apps/showcase/.../booking/data/booking.transformer.ts | Basic transformer with snake_case ↔ camelCase mapping |
apps/showcase/.../booking/data/booking.data-services.ts | Data service with injected transformer |
apps/showcase/.../booking/data/advanced-booking.transformer.ts | Extended transformer with custom chart method |
apps/showcase/.../booking/data/advanced-booking.data-services.ts | Extended service with custom getAvailabilityChart() |