¿Mapper, o proyección inline?
Hay dos mappers por entidad, en direcciones opuestas, y nunca en el mismo fichero:
fila de Prisma → [persistence mapper] → Entidad → [presentation mapper] → DTO
| Dónde | Qué hace | |
|---|---|---|
| Persistencia | infrastructure/persistence/<x>/x.mapper.ts |
toDomain(row), y toPersistence(entity) si se escribe |
| Presentación | presentation/<x>/x-response.mapper.ts |
toDto(entity) |
En petid-api son 11 y 10. Que no sean 11 y 11 es el caso interesante, y está al final.
Por qué existen aunque sean de una línea
export class PetTagMapper {
static toDomain(row: PetTagRow): PetTag {
return new PetTag(row);
}
}
Eso es todo el fichero. Parece ceremonia, y lo es hoy — pero es la costura: el día que añadas una columna que la entidad no tiene, o renombres un campo, hay un sitio evidente donde ponerlo y ninguna llamada a Prisma repartida por ahí que actualizar.
Son clases con métodos static. No se inyectan, no tienen estado.
La función que sí justifica la capa: lista blanca
UserResponseMapper no copia el objeto, enumera campo a campo:
static toDto(user: User): UserResponseDto {
return {
id: user.id,
email: user.email,
role: user.role,
emailVerifiedAt: user.emailVerifiedAt,
twoFactorEnabledAt: user.twoFactorEnabledAt,
// ...
};
}
User tiene password y twoFactorSecret. Al enumerar, esos campos no pueden salir por HTTP
ni aunque alguien los añada a la entidad mañana. Con un spread ({ ...user }) saldrían solos.
Escribe siempre el mapper de presentación enumerando campos. Nunca
{ ...entity }, aunque hoy la entidad no tenga nada sensible.
Mantenlo puro: lo calculado entra por parámetro
// url la resuelve el llamante (buildResourceDownloadUrl) — así esto sigue
// siendo un mapper puro, y la URL nunca se persiste
static toDto(resource: Resource, url: string): ResourceResponseDto
El mapper no firma la URL ni consulta nada. Si un DTO necesita un dato que no está en la entidad, lo recibe. Un mapper que inyecta dependencias ha dejado de ser un mapper.
El único caso sin mapper de presentación
pet-tags no tiene pet-tag-response.mapper.ts. Su DTO es un campo:
// pet-tag.controller.ts
const tag = await this.getOrCreatePetTagUC.execute(petId, user);
return { publicCode: tag.publicCode };
Crear una clase con un método estático para copiar un campo es más ruido que valor.
Mapper de persistencia siempre. Mapper de presentación salvo que el DTO sea una proyección de uno o dos campos.
El límite práctico: si al escribir el mapper te sale un return de una o dos líneas y no hay nada
que ocultar, hazlo inline en el controller. En cuanto haya tres campos, hay riesgo de que el cuarto
sea sensible — y entonces toca mapper.
Variantes: métodos, no mappers nuevos
Cuando una entidad se sirve de varias formas, van como métodos del mismo mapper:
UserResponseMapper.toDto(user) // usuario a secas
UserResponseMapper.toProfileDto(user, name, lastName) // + datos de perfil
UserResponseMapper.toProfileDtoFromProfile(user, profile)
No un UserWithProfileResponseMapper aparte.