Cuando construyes una API en NestJS, proteger el password con hashing no basta: también necesitas evitar que ese campo viaje en las respuestas. Aquí entra la serialización, una práctica clave para devolver solo la información que tu cliente debe ver.
La idea es simple. Aunque ya guardes contraseñas con hashing, exponerlas en el JSON de respuesta sigue siendo un riesgo y un ruido innecesario. Serializar significa transformar la salida de tus entidades, y eso incluye excluir campos sensibles antes de que lleguen al cliente.
¿Qué significa serializar datos en una API NestJS?
Serializar es el proceso de transformar los valores de una entidad antes de devolverlos en la respuesta. Esa transformación puede incluir convertir tipos, ajustar formatos o, como en este caso, excluir columnas que no deberían salir nunca.
¿Qué es serializar en una API? Es transformar la salida de tus datos antes de enviarlos al cliente, lo que permite ocultar campos sensibles como el password sin tocar la base de datos.
En NestJS, esta tarea la hace el ClassTransformer, el mismo paquete que se instala junto al ClassValidator cuando configuras validaciones. Ya lo tienes ahí, solo falta activarlo.
¿Cómo uso el decorador Exclude para ocultar el password?
El primer paso vive en tu entity. Importas Exclude desde class-transformer y lo colocas como decorador encima de la columna que quieres ocultar.
ts
import { Exclude } from 'class-transformer';
@Entity()
export class User {
// otras columnas
@Exclude()
@Column()
password: string;
}
Con eso le dices a NestJS: "cada vez que serialices esta entidad, deja fuera el password". Pero si pruebas el endpoint en Postman en este punto, vas a ver que el campo sigue apareciendo. Falta habilitar la transformación a nivel global.
¿Por qué el decorador Exclude no funciona solo?
Porque el decorador es una marca, no un ejecutor. NestJS necesita saber que debe correr el ClassTransformer en cada respuesta. Eso se configura en el archivo main.ts con dos piezas: las opciones de transformación y un interceptor global.
¿Cómo configuro ClassSerializerInterceptor de forma global?
En tu main.ts ya deberías tener un ValidationPipe con transform: true. Ahora le agregas transformOptions para habilitar las conversiones implícitas y registras el ClassSerializerInterceptor para toda la aplicación.
ts
import { ValidationPipe, ClassSerializerInterceptor } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector)),
);
El ClassSerializerInterceptor viene de @nestjs/common y necesita una instancia de Reflector, que obtienes desde @nestjs/core. Con esa configuración, NestJS aplica la serialización en cada respuesta sin que tengas que tocar nada más.
¿Qué hace ClassSerializerInterceptor en NestJS? Intercepta las respuestas de tus controllers y aplica las reglas de class-transformer, como @Exclude(), sobre cualquier entidad que devuelvas.
¿Por qué excluir a nivel de entidad ahorra tanto trabajo?
Aquí está lo interesante: al colocar @Exclude() en la entity y registrar el interceptor de forma global, no necesitas ir endpoint por endpoint removiendo el password a mano.
Cualquier punto de tu API que retorne la entidad User aplicará la exclusión automáticamente. Esto vale para varios casos típicos:
- El endpoint que lista todos los usuarios deja de mostrar el password.
- El
getUser que trae uno por id también lo oculta.
- El
create, al devolver el usuario recién guardado, responde sin el campo sensible.
La razón es que todas esas operaciones pasan por el repository de TypeORM, y el repository siempre se apoya en la entity. Si la entity tiene el @Exclude(), la regla se aplica en cascada.
¿Qué pasa cuando creo un usuario nuevo?
Si haces un POST para crear, por ejemplo, un "usuario cinco", el flujo interno usa create, save y findOne del repository. Como el resultado final es una instancia de la entity User, el interceptor la serializa antes de responder y el password desaparece del JSON.
Este patrón te da una capa de seguridad consistente: la entidad define qué se expone y qué no, y la API entera respeta esa decisión sin código repetido. Si más adelante quieres ocultar otro campo, basta con agregar @Exclude() en la columna correspondiente.
¿Has tenido que limpiar respuestas campo por campo en proyectos anteriores? Cuéntame en los comentarios cómo manejabas la serialización antes de conocer este patrón.