Guardar contraseñas en texto plano dentro de la base de datos es una de las peores prácticas de seguridad que puedes cometer al construir una API. Con NestJS, TypeORM y la librería Bcrypt puedes proteger esa información sensible mediante hashing, de forma que ni siquiera tú como administrador puedas leerla. Esta guía es para desarrolladores backend que ya tienen una entidad de usuarios y quieren blindarla.
Por qué no debes guardar contraseñas en texto plano
Cuando revisas la tabla de usuarios y ves los passwords en crudo, tienes un problema serio de seguridad. El único que debería conocer la contraseña es el propio usuario, ni tú como dueño de la arquitectura, ni los desarrolladores con acceso a la base.
La solución estándar es aplicar hashing, una transformación de una sola vía que convierte la contraseña en una cadena ilegible. Aunque alguien comprometa tu base de datos, lo único que verá será el hash, no la contraseña original [0:30].
¿Qué es el hashing de contraseñas? Es un proceso que transforma una contraseña en una cadena cifrada irreversible. A diferencia del cifrado, no existe una función inversa: solo puedes comparar hashes para verificar coincidencias.
Cómo instalar y usar Bcrypt en NestJS
La documentación oficial de NestJS, en su sección de security, recomienda usar la librería Bcrypt para hashing de contraseñas. Necesitas instalar tanto el paquete como sus tipados de TypeScript para que funcione correctamente con tu proyecto.
Ejecuta en tu terminal los dos comandos de instalación: uno para bcrypt y otro para @types/bcrypt. Sin los tipos vas a tener errores al importar la librería en tu código.
Bcrypt expone dos métodos clave que vas a usar siempre:
hash(password, saltRounds): convierte la contraseña en hash. Por defecto las iteraciones son 10.
compare(rawPassword, hash): verifica si una contraseña en crudo corresponde a un hash existente.
- Las iteraciones controlan qué tan costoso es generar el hash. Más iteraciones, más seguridad, pero también más tiempo de cómputo [2:35].
Esa comparación es la que te permite autenticar al usuario sin necesidad de almacenar nunca su contraseña real.
Cómo aplicar el hook BeforeInsert en TypeORM
Para que el hasheo ocurra automáticamente cada vez que se cree un usuario, TypeORM ofrece un decorador llamado @BeforeInsert. Este hook ejecuta lógica justo antes de que un registro se inserte en la base de datos.
Dentro de tu entidad User, importa BeforeInsert desde typeorm e importa bcrypt. Crea un método asíncrono que sobrescriba la propiedad password con su versión hasheada antes de la inserción.
typescript
import { BeforeInsert, Column, Entity } from 'typeorm';
import * as bcrypt from 'bcrypt';
@Entity()
export class User {
@Column()
password: string;
@BeforeInsert()
async hashPassword() {
this.password = await bcrypt.hash(this.password, 10);
}
}
El número 10 son los salt rounds. Si quieres mayor flexibilidad, puedes leerlo desde una variable de entorno usando el módulo de configuración de NestJS [4:15].
Por qué debes usar create en lugar de save
Aquí está el detalle que muchos desarrolladores pasan por alto: el método save() del repositorio no ejecuta los hooks como @BeforeInsert. Si llamas directamente a repository.save(data), tu contraseña se guardará en texto plano y el decorador será ignorado.
La solución es llamar primero al método create(), que construye la instancia de la entidad y dispara los hooks, y luego pasar el resultado a save().
¿Cuál es la diferencia entre create y save en TypeORM? create() solo construye la estructura de la entidad en memoria y ejecuta los hooks. save() persiste los datos en la base. Necesitas ambos para que el hasheo automático funcione.
El flujo correcto en tu service queda así:
typescript
async createUser(body: CreateUserDto) {
const newUser = this.userRepository.create(body);
const savedUser = await this.userRepository.save(newUser);
return this.findOne(savedUser.id);
}
Fíjate que renombro la variable a savedUser para diferenciarla de cualquier otro getUser que tengas en el servicio. Ese findOne final te permite devolver el usuario con el formato exacto que quieres exponer al cliente [6:50].
Cómo verificar que el hash se aplicó correctamente
Después de aplicar estos cambios, los usuarios que ya existían en tu base seguirán con su contraseña en crudo. Para arreglarlos necesitarías una migración manual que recorra los registros y aplique el hashing uno por uno.
Los usuarios nuevos sí van a guardarse correctamente. Cuando llames al endpoint de crear usuario con un password como admin123, la respuesta y la base de datos mostrarán una cadena hasheada del estilo $2b$10$... en lugar del valor original [8:20].
Cualquier persona con acceso a la base no podrá saber cuál era la contraseña original. La protección es real porque solo el usuario, al ingresar su contraseña en el login, puede validarla mediante bcrypt.compare.
Qué falta por resolver en la respuesta del endpoint
Queda un detalle pendiente. Aunque la contraseña ya esté hasheada en la base, el endpoint sigue devolviendo el campo password en respuestas como getUsers o al crear un nuevo registro. Esa información no debería viajar nunca al cliente, ni siquiera hasheada.
¿Cómo excluirías ese campo de manera automática para que nunca se serialice en las respuestas? Cuéntame en los comentarios qué estrategia usarías: ¿interceptores, class-transformer con @Exclude, o un DTO de respuesta dedicado?