Mejores practicas para APIs Seguras en NestJS: Una Guía desde la Experiencia
Descubre prácticas efectivas para proteger tu backend en NestJS con estrategias probadas que he implementado en proyectos reales.
Descubre prácticas efectivas para proteger tu backend en NestJS con estrategias probadas que he implementado en proyectos reales.
Después de varios proyectos backends y microservicios para e-commerce y plataformas web con NestJS, quiero compartir algunas de las lecciones que he aprendido sobre cómo desarrollar APIs seguras, un factor muy importante cuando las amenazas cibernéticas son cada vez más comunes.
Una de las grandes ventajas de este framework es su arquitectura modular, que nos permite desarrollar módulos con funcionalidades específicas e independientes. Esto no solo mantiene el código organizado, sino que también facilita la escalabilidad y el mantenimiento en proyectos grandes. Al estar basado en Express y usar TypeScript por defecto, la transición es bastante natural para quienes ya trabajan con Node.js.
Un DTO (Data Transfer Object) es un objeto que define la estructura y formato de los datos esperados en una solicitud. Se usa para garantizar que los datos sean correctos y facilitar su validación.
En NestJS, los DTOs se crean como clases TypeScript y permiten definir la estructura de los datos que nuestra API espera recibir. Un ejemplo de DTO para crear un usuario podría ser:
import { IsString, IsEmail, IsNotEmpty } from 'class-validator';
export class CreateUserDTO {
@IsString()
@IsNotEmpty()
name: string;
@IsEmail()
email: string;
@IsString()
@IsNotEmpty()
password: string;
}
Aquí usamos decoradores de la librería class-validator
para aplicar reglas de validación.
ValidationPipe
ValidationPipe
nos ofrece un mecanismo eficiente para validar y transformar los datos de entrada, asegurando que cumplan con las reglas definidas en los DTOs medianteclass-validator
.
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateUserDTO } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
createUser(@Body() createUserDTO: CreateUserDTO) {
return { message: 'Usuario creado', data: createUserDTO };
}
}
De esta forma, si los datos enviados no cumplen con las validaciones definidas en el DTO, NestJS responderá con un error 400 indicando los problemas en la solicitud.
Un Guard en NestJS es un mecanismo que controla el acceso a rutas antes de que el controlador procese la solicitud. Se usa comúnmente para autenticación y autorización.
Los Guards permiten definir reglas de acceso a los endpoints. Se ejecutan antes de que la solicitud llegue al controlador. Un ejemplo de Guard para autenticar usuarios mediante un token JWT sería:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
import { verify } from 'jsonwebtoken';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request: Request = context.switchToHttp().getRequest();
const token = request.headers['authorization'];
if (!token) return false;
try {
verify(token, 'SECRET_KEY'); // Verifica el token JWT
return true;
} catch {
return false;
}
}
}
Podemos aplicar Guards de tres maneras:
Globalmente (se aplica a toda la aplicación):
app.useGlobalGuards(new AuthGuard());
A nivel de controlador:
@UseGuards(AuthGuard)
@Controller('users')
export class UsersController {
A nivel de una ruta específica:
@UseGuards(AuthGuard)
@Get('profile')
getProfile() {
return { message: 'Perfil del usuario' };
}
Para que comprendamos mejor cómo se relacionan los Guards, Pipes y Middleware, es útil entender el ciclo de vida de una petición en NestJS:
He visto cómo los ataques de fuerza bruta pueden colapsar un servidor. El rate limiting es crucial:
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot({
ttl: 60,
limit: 10,
}),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
Un consejo que me hubiera gustado recibir cuando empecé: configurar los headers HTTP adecuados es sorprendentemente efectivo:
import helmet from 'helmet';
app.use(helmet());
Nunca, jamás, guardes secretos en tu código:
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
// ...otros módulos
],
})
export class AppModule {}
// En otros servicios
@Injectable()
export class AuthService {
constructor(private configService: ConfigService) {
// Accede a las variables de entorno de manera segura
const jwtSecret = this.configService.get<string>('JWT_SECRET');
}
}
En mis primeros proyectos, me encontré con problemas por no configurar CORS adecuadamente:
app.enableCors({
origin: ['https://tudominio.com', 'https://admin.tudominio.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
});
No subestimes el valor de un buen sistema de logs:
import { Logger } from '@nestjs/common';
@Injectable()
export class UserService {
private readonly logger = new Logger(UserService.name);
async createUser(createUserDto: CreateUserDto) {
this.logger.log(`Creando usuario: ${createUserDto.username}`);
// Lógica para crear usuario
this.logger.verbose(`Usuario creado con ID: ${userId}`);
}
}
Proximamente disponible.