Introducción
Cuando desarrollamos aplicaciones frontend modernas con React y TypeScript, una práctica común es consumir datos directamente desde APIs backend. Sin embargo, utilizar la estructura de respuesta de la API de manera directa en los componentes crea un acoplamiento innecesario que eventualmente genera problemas de mantenibilidad, escalabilidad y testabilidad.
Este artículo explora cómo implementar correctamente tres conceptos fundamentales: Data Transfer Objects (DTO), Modelos de Dominio y Mappers, con un ejemplo práctico completo.
El problema fundamental
Considere el siguiente endpoint de la API JSONPlaceholder:
GET https://jsonplaceholder.typicode.com/users
La respuesta típica es:
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz"
}
La pregunta que surge naturalmente es: ¿debería utilizar esta estructura tal como viene del backend directamente en los componentes de React?
La respuesta es: no necesariamente. Y la razón fundamental es que la estructura de transferencia de datos y la estructura de dominio de la aplicación tienen propósitos distintos.
1. DTO — Data Transfer Object
¿Qué es un DTO?
Un DTO es una representación que refleja exactamente cómo los datos viajan entre sistemas: entre el backend y el frontend. Es un contrato de comunicación que no contiene lógica de negocio ni validaciones.
Características de un DTO:
- Representa la estructura exacta de la API
- No contiene lógica de negocio
- No contiene métodos o comportamientos
- Cambia cuando la API cambia
- Es específico del contrato de comunicación
Implementación
// src/infrastructure/dto/user.dto.ts
export type UserDto = {
id: number;
name: string;
username: string;
email: string;
};
Este archivo actúa como una "fotografía fiel" del contrato de la API. Si el backend modifica la estructura de respuesta, este archivo es el único lugar donde debe reflejarse ese cambio inicialmente.
2. Modelo — Domain Model
¿Qué es un modelo de dominio?
El modelo representa cómo la aplicación entiende, estructura y utiliza los datos internamente. No depende de cómo el backend decida transportarlos. Depende exclusivamente de las necesidades del dominio de la aplicación.
Características de un modelo:
- Representa conceptos del dominio de la aplicación
- No depende de la estructura de la API
- Puede contener propiedades derivadas o transformadas
- Refleja la realidad del negocio
- Es estable respecto a cambios en la API
Implementación
// src/domain/models/user.model.ts
export type User = {
id: number;
fullName: string;
email: string;
};
Observe las diferencias intencionales con respecto al DTO:
-
namese convierte enfullName: más descriptivo para el dominio -
usernamese elimina: no es relevante para el contexto de la aplicación
Esta transformación es deliberada. El modelo representa qué necesita saber la aplicación, no qué envía la API.
3. Mapper — El traductor
¿Qué es un mapper?
Un mapper es una función pura que realiza la traducción entre dos representaciones de datos: del DTO al Modelo y, potencialmente, del Modelo al DTO.
Características de un mapper:
- Es una función pura sin estado
- No tiene efectos secundarios
- Es determinístico: misma entrada, misma salida siempre
- Centraliza la lógica de transformación
- Facilita el testing
Implementación
// src/infrastructure/mappers/user.mapper.ts
import type { UserDto } from '../dto/user.dto';
import type { User } from '../../domain/models/user.model';
export const mapUserDtoToUser = (dto: UserDto): User => ({
id: dto.id,
fullName: dto.name,
email: dto.email,
});
Este archivo es el punto de acoplamiento controlado entre la API externa y la lógica interna de la aplicación. Cualquier cambio en la estructura de la API se aísla aquí.
4. Diagrama de flujo de datos
El flujo asegura una separación clara:
- La API devuelve un DTO
- El mapper transforma el DTO al modelo de dominio
- El componente nunca conoce la existencia del DTO
- El modelo es la única interface con la cual el componente interactúa
5. Implementación del hook específico
// src/presentation/hooks/useUsers.ts
import { useEffect, useState } from 'react';
import { mapUserDtoToUser } from '../../infrastructure/mappers/user.mapper';
import type { User } from '../../domain/models/user.model';
import type { UserDto } from '../../infrastructure/dto/user.dto';
export const useUsers = () => {
const [users, setUsers] = useState<User[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
const fetchUsers = async () => {
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users',
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const dtos: UserDto[] = await response.json();
const mappedUsers = dtos.map(mapUserDtoToUser);
setUsers(mappedUsers);
setError(null);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchUsers();
return () => controller.abort();
}, []);
return { users, loading, error };
};
Observe que este hook:
- Realiza la transformación de DTO a modelo dentro de la lógica de obtención
- Retorna exclusivamente el modelo de dominio
- Mantiene la obtención de datos y la transformación coordinadas
6. Componente limpio
// src/presentation/components/Users.tsx
import { useUsers } from '../hooks/useUsers';
export function Users() {
const { users, loading, error } = useUsers();
if (loading) {
return <p>Cargando usuarios...</p>;
}
if (error) {
return <p>Error al cargar usuarios: {error}</p>;
}
if (!users || users.length === 0) {
return <p>No hay usuarios disponibles</p>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.fullName}</strong>
<p>{user.email}</p>
</li>
))}
</ul>
);
}
Este componente tiene características importantes:
- No importa ni conoce la existencia de UserDto
- No contiene lógica de transformación
- No necesita saber cómo se estructura la API
- Es completamente agnóstico respecto a la infraestructura
- Solo conoce el modelo de dominio y lo renderiza
7. Testing
La separación de responsabilidades facilita significativamente el testing:
// src/infrastructure/mappers/user.mapper.test.ts
import { mapUserDtoToUser } from './user.mapper';
import type { UserDto } from '../dto/user.dto';
describe('User Mapper', () => {
it('should map UserDto to User correctly', () => {
const dto: UserDto = {
id: 1,
name: 'Leanne Graham',
username: 'Bret',
email: 'Sincere@april.biz',
};
const result = mapUserDtoToUser(dto);
expect(result).toEqual({
id: 1,
fullName: 'Leanne Graham',
email: 'Sincere@april.biz',
});
});
it('should handle empty strings in name field', () => {
const dto: UserDto = {
id: 2,
name: '',
username: 'test',
email: 'test@example.com',
};
const result = mapUserDtoToUser(dto);
expect(result.fullName).toBe('');
});
});
El mapper, al ser una función pura, es trivial de testear sin necesidad de mocks, stubs o configuraciones complejas.
8. Ventajas de esta arquitectura
Bajo acoplamiento
Los componentes no dependen de la estructura de la API. Cambios en el backend se contienen en la capa de mappers.
Mantenibilidad
Cuando la API cambia, existe un lugar único donde documentar y realizar la transformación.
Escalabilidad
Cuando la aplicación crece, cada responsabilidad está claramente delimitada. Nuevos desarrolladores pueden entender rápidamente dónde va cada cosa.
Testabilidad
Cada capa puede ser testeada independientemente. Los mappers son funciones puras sin dependencias externas.
Reutilización
Los mappers pueden usarse en múltiples contextos: en componentes, en stores de estado, en service workers, etc.
Flexibilidad
Es posible tener múltiples modelos del mismo concepto (por ejemplo: UserSummary, UserDetail, UserProfile) que mapean del mismo DTO pero contienen diferente información.
Estructura de carpetas recomendada
src/
├── domain/
│ ├── models/
│ │ └── user.model.ts
│ └── interfaces/
├── infrastructure/
│ ├── dto/
│ │ └── user.dto.ts
│ ├── mappers/
│ │ ├── user.mapper.ts
│ │ └── user.mapper.test.ts
│ └── api/
│ └── client.ts
├── presentation/
│ ├── components/
│ │ └── Users.tsx
│ └── hooks/
│ └── useUsers.ts
└── App.tsx
Esta estructura refleja la arquitectura de capas:
- Domain: Conceptos de negocio puros
- Infrastructure: Detalles técnicos de integración
- Presentation: Componentes de React
Conclusión
Implementar correctamente la separación entre DTOs, modelos y mappers en aplicaciones React + TypeScript no es un sobre-ingenierización. Es una práctica arquitectónica que proporciona claridad, mantenibilidad y flexibilidad.
Los beneficios se hacen evidentes especialmente cuando:
- El equipo crece
- La API evoluciona
- Los requisitos del negocio cambian
- Es necesario reutilizar datos en contextos diferentes
El patrón DTO-Mapper-Modelo es el mismo principio utilizado en arquitecturas empresariales, adaptado e implementado correctamente en aplicaciones frontend modernas.

Top comments (0)