Saltar al contenido

Clase 20 - Middleware y Nest

#ts #server #backend #nest #middleware #try-catch #error-exception

Middleware y manejo de errores

Middleware

Un middleware es una función o conjunto de funciones que se utilizan en una aplicación web para procesar las solicitudes entrantes y salientes. Middleware

Router

Son una parte fundamental de la estructura de la aplicación. Permiten agrupar rutas relacionadas y definir manejadores de solicitudes para esas rutas específicas. Router

Errores y Excepciones

  • Errores: los errores son situaciones inesperadas que ocurren durante la ejecución de un programa y que interrumpen su funcionamiento normal.

  • Excepciones: Las excepciones son eventos que ocurren durante la ejecución de un programa y que representan situaciones excepcionales que pueden ser manejadas.

  • Try-catch: las excepciones se gestionan mediante bloques de código llamados try-catch

try {
// Código en el cual se espera una respuesta exitosa
} catch(error) {
// Código en caso de error
} finally {
// Acciones que deben ejecutarse siempre ya se que tengamos una respuesta exitosa o error
}

Conceptos claves de NestJS

Características y ventajas

  • Es un framework para construir aplicaciones del lado del servidor (backend) que se basa en Node.js
  • Utiliza Typescript
  • Se enfoca en mejorar la modularidad y la mantenibilidad del código. Se considera un framework progresivo y está diseñado para crear aplicaciones escalables y altamente mantenibles.
  • Completamente modular, lo que significa que las aplicaciones se dividen en módulos reutilizables y fácilmente configurables. Esto facilita que el código sea más organizado y escalable, ideal para grandes aplicaciones empresariales.
  • Ecosistema robusto: Cuenta con un ecosistema extensible y compatible con bibliotecas y herramientas de terceros como TypeORM, Mongoose, Passport (para autenticación), GraphQL, y WebSockets.
  • Está construido sobre Express, lo que significa que es completamente compatible con el ecosistema de paquetes de Node.js. Esto permite integrar fácilmente bibliotecas y herramientas que ya son parte del flujo de trabajo de desarrollo en Node.js, pero con una estructura más robusta y un enfoque en la mantenibilidad del código.

Fundamentos de NestJS

Módulos

  • Está organizado en módulos, que son la unidad fundamental donde agruparemos componentes relacionados, como controladores, servicios, etc.
  • Aquí veremos el primer decorador @Module() el cual recibirá un objeto con propiedades como
    • controllers: Define los controladores que pertenecen al módulo.
    • providers: Define los proveedores (servicios, interceptores, filtros, etc.) que están disponibles en el módulo.
    • imports: Permite importar otros módulos que este módulo necesita.
    • exports: Permite que otros módulos utilicen los proveedores de este módulo.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [], // Aquí se pueden importar otros módulos
controllers: [UsersController], // Se registran los controladores
providers: [UsersService], // Se registran los servicios (y otros proveedores)
})
export class UsersModule {}

Controladores

  • Son responsables de manejar las solicitudes entrantes y devolver las respuestas adecuadas al cliente.
  • Cada controlador está asociado a un conjunto de rutas HTTP y se especializa en manejar las solicitudes relacionadas con un recurso específico (por ejemplo, usuarios, productos, etc.).
  • Asi como vimos el decorador @Module() aquí tenemos el decorador @Controller()
  • Y dentro del controlador veremos los decoradores encargados de manejar las rutas (@Get()@Post()@Put()@Delete()).
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users') // Prefijo de ruta: "/users"
export class UsersController {
constructor(private readonly usersService: UsersService) {} // Inyectamos el servicio
@Get() // Ruta GET "/users"
findAll() {
return this.usersService.findAll();
}
@Get(':id') // Ruta GET "/users/:id"
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@Post() // Ruta POST "/users"
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}

Servicios

  • Me van a permitir mantener el código limpio para que el controlador solo se encargue de manejar las solicitudes HTTP
  • Centralizan la lógica de negocio y permiten la reutilización en diferentes partes de la aplicación.
  • Facilitan la escritura de pruebas unitarias, ya que la lógica puede ser probada de forma aislada.
  • Aquí aparece un nuevo decorador que será @Injectable() el cual permite que Nest pueda inyectar automáticamente este servicio en otros componentes (como los controladores)
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = [];
findAll() {
return this.users; // Devuelve todos los usuarios
}
findOne(id: string) {
return this.users.find(user => user.id === id); // Busca un usuario por su ID
}
create(user) {
this.users.push(user); // Añade un nuevo usuario
}
}

Providers

  • En Nest, cualquier clase que sea inyectable puede ser considerada un provider. Los providers pueden ser servicios, repositorios, clases de utilidades o cualquier otra clase que NestJS gestione mediante Inyección de Dependencias (DI).
  • Se registran en los módulos y luego se pueden inyectar en otros proveedores o controladores.
  • ***Inyección de dependencias:
    • Es uno de los principios claves de NestJS y es justamente lo que facilita la creación de aplicaciones altamente desacopladas
    • NestJS tiene un contenedor de inyección de dependencias entonces cuando declaro un servicio o proveedor con el decorador @Injectable(), Nest lo registra en ese contenedor
    • Luego los controladores o servicios que necesiten usar ese proveedor lo pueden inyectar mediante el constructor de la clase
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {} // UsersService inyectado
@Get()
findAll() {
return this.usersService.findAll(); // Usa el servicio inyectado
}
}

Decoradores

  • Son funciones especiales que se aplican a clases, métodos o parámetros para modificar su comportamiento. Por ejemplo
  • Decoradores de clase@Controller()@Injectable()
  • Decoradores de método@Get()@Post()@Put()@Delete()
  • Decoradores de parámetro@Param()@Body()@Query()
@Controller('users')
export class UsersController {
@Get(':id')
getUser(@Param('id') id: string) {
return `User ID: ${id}`;
}
@Post()
createUser(@Body() createUserDto: CreateUserDto) {
return `User created: ${createUserDto.name}`;
}
}

Middlewares

Características

  • Son funciones que se ejecutan antes de que una solicitud llegue al controlador
  • Pueden aplicarse a todas las rutas o a rutas específicas de un módulo o controlador.
  • Los middlewares en NestJS son similares a los de Express.js, ya que NestJS se basa en Express por defecto
  • Un middleware en NestJS puede ser una función o una clase que implemente la interfaz NestMiddleware. Lo más común es implementarlo como una clase con un método use()
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`Request made to: ${req.url}`);
next(); // Es importante llamar a next() para continuar el flujo de la solicitud
}
}
  • Para aplicar un middleware, se debe hacerlo en el módulo, específicamente dentro del método configure() de una clase que implemente la interfaz NestModule. Aquí es donde se especifican las rutas a las que se aplicará el middleware.
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { UsersController } from './users.controller';
import { LoggerMiddleware } from './logger.middleware';
@Module({
controllers: [UsersController],
})
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware) // Aplicamos el middleware
.forRoutes(UsersController); // Definimos las rutas a las que se aplica (en este caso, al controlador 'users')
.apply(OtroMiddleware) // Puedo aplicar todos los middleware que quiera
.forRoutes(OtherRoute)
}
}
  • También es posible aplicarlo a rutas más detalladas
import { RequestMethod } from '@nestjs/common';
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'users/:id', method: RequestMethod.GET }); // Se aplica solo a la ruta GET /users/:id
  • Y es posible aplicarlo a modo global en el archivo main.ts
const app = await NestFactory.create(AppModule);
app.use(LoggerMiddleware);

Middleware vs. Guardias, Filtros e Interceptores:

Es importante entender que, aunque los middlewares cumplen tareas similares a otras características de NestJS como los guardiasfiltros e interceptores, cada uno tiene un propósito específico:

  • Middlewares: Actúan en todas las solicitudes (generalmente a nivel de servidor o módulo) antes de llegar a los controladores.
  • Guardias (Guards): Se usan para aplicar reglas de autorización, permitiendo o denegando el acceso a una ruta específica.
  • Filtros de Excepciones (Exception Filters): Gestionan los errores o excepciones que se producen durante el manejo de una solicitud.
  • Interceptores: Modifican las solicitudes o respuestas antes o después de ser procesadas por el controlador, pueden transformar la respuesta o medir tiempos de ejecución.

Pipes y validaciones en NestJS

  • Los Pipes son mecanismos que permiten transformar y validar los datos de entrada antes de que lleguen a los controladores.
  • Te aseguran que los datos que recibe tu aplicación sean correctos, estén bien formateados, y cumplan con ciertas reglas antes de ser procesados.
  • Es una clase que implementa la interfaz PipeTransform y se encarga de dos tareas principales:
    1. Transformación: Convierte los datos de entrada en el formato deseado antes de que lleguen al controlador. Por ejemplo, puedes transformar una cadena en un número.
    2. Validación: Valida los datos de entrada y, si no son válidos, lanza una excepción que NestJS puede capturar y manejar.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata) {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed: Value is not a number');
}
return val;
}
}
  • Para aplicar los pipes tenemos 3 formas:
  • A nivel de parámetro: Se aplica a un parámetro específico de un controlador.
@Get(':id')
getUser(@Param('id', ParseIntPipe) id: number) {
return `User ID is ${id}`;
}
  • A nivel de método: Se aplica a todos los parámetros del método de un controlador.
@Post()
@UsePipes(new ValidationPipe())
createUser(@Body() createUserDto: CreateUserDto) {
// Crea un nuevo usuario
}
  • A nivel global: Se aplica a toda la aplicación para que todas las solicitudes pasen por el pipe.
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
  • También viene con algunos pipes integrados que se pueden usar directamente:
  1. ValidationPipe: Valida los datos basados en clases DTO y decoradores de validación.
  2. ParseIntPipe: Convierte los parámetros de la solicitud a enteros.
  3. ParseBoolPipe: Convierte valores a booleanos.
  4. ParseArrayPipe: Convierte cadenas separadas por comas a arrays.

Guards

  • Se utiliza principalmente para verificar si una solicitud tiene los permisos o condiciones necesarias para continuar. Su función principal es decidir si una solicitud entrante debe ser manejada por el controlador o no, lo que los convierte en una pieza clave para la autenticación y autorización.
  • Implementan la interfaz CanActivate. Esta interfaz define un único método canActivate(), que debe devolver un valor booleano o una promesa que resuelva un booleano. Si el guard devuelve true, la solicitud puede continuar hacia el controlador; si devuelve false, la solicitud es bloqueada.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user; // Imagina que el usuario ya fue autenticado
return !!user; // Si hay un usuario autenticado, continúa la solicitud
}
}
  • Pueden ser aplicados a controladores o métodos específicos
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
@Controller('users')
export class UsersController {
@Get()
@UseGuards(AuthGuard) // Aplica el AuthGuard a este método
getUsers() {
return 'This route is protected by a guard';
}
}
  • Y también pueden ser aplicados a nivel global
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './auth.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new AuthGuard()); // Aplica el guard globalmente
await app.listen(3000);
}
bootstrap();

Interceptors

  • Se utiliza para modificar o manipular el flujo de las solicitudes o respuestas, ya sea antes o después de que el controlador maneje la solicitud
  • Son útiles para tareas como la transformación de respuestas, el manejo de excepciones, el registro de tiempos de ejecución
  • Es una clase que implementa la interfaz NestInterceptor y contiene un método intercept(). Este método recibe dos parámetros: el contexto de ejecución (ExecutionContext) y el CallHandler, que es una referencia al siguiente paso en el pipeline (normalmente el controlador).
  • El intercept() puede modificar la solicitud antes de que llegue al controlador, o puede interceptar la respuesta para modificarla antes de que se envíe de vuelta al cliente.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`))
);
}
}
  • Puede ser aplicado a nivel de controlador o método mediante @UseInterceptors()
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('users')
export class UsersController {
@Get()
@UseInterceptors(LoggingInterceptor) // Aplica el interceptor
getUsers() {
return 'This route is intercepted';
}
}
  • O a nivel global de forma similar a los guards
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor()); // Interceptor global
await app.listen(3000);
}
bootstrap();
  • Casos comunes de uso de Interceptores:

    1. Transformación de respuestas: Modificar la estructura de la respuesta antes de enviarla al cliente, por ejemplo, eliminando datos sensibles o agregando metadatos.
    2. Manejo de excepciones: Capturar y modificar las excepciones antes de que se devuelvan como respuestas, o transformar las excepciones de una forma personalizada.
    3. Registro de tiempos de ejecución: Medir el tiempo que toma una operación dentro del controlador y registrar esta información para monitoreo.
    4. Cacheo de respuestas: Implementar un mecanismo de cacheo para evitar llamadas repetitivas a controladores cuando la respuesta no cambia frecuentemente.
  • En el siguiente ejemplo agrega un mensaje adicional a la respuesta original

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
map(data => ({ data, message: 'Operation successful' })) // Agrega un mensaje a la respuesta
);
}
}

Filtros de Excepciones

  • Sirven para capturar y manejar los errores que ocurren durante la ejecución de una solicitud. Su objetivo principal es capturar las excepciones lanzadas por cualquier parte del código y devolver respuestas controladas y personalizadas, lo que mejora la robustez y experiencia del usuario al interactuar con la aplicación
  • Implementa la interfaz ExceptionFilter.
  • NestJS proporciona un filtro de excepciones global por defecto que maneja todos los errores no capturados, pero puedes definir tus propios filtros para tener un control más fino sobre cómo se manejan ciertos tipos de excepciones.
  • Un filtro de excepciones tiene que implementar el método catch(), que recibe dos parámetros:
    1. La excepción lanzada (por ejemplo, un error HTTP o un error personalizado).
    2. El contexto de ejecución (ExecutionContext), que proporciona información sobre la solicitud actual.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException) // Este filtro solo atrapará excepciones de tipo HttpException
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message || 'Error ocurrido',
});
}
}
  • Se pueden aplicar a nivel controlador o método y también a nivel global
import { Controller, Get, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';
@Controller('users')
export class UsersController {
@Get()
@UseFilters(HttpExceptionFilter)
getUsers() {
throw new HttpException('Forbidden', 403); // Lanza una excepción que será atrapada por el filtro
}
}
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter()); // Filtro global para toda la app
await app.listen(3000);
}
bootstrap();
  • Con el decorador @Catch() elijo qué excepciones capturar
@Catch(HttpException, AnotherException)
export class MultipleExceptionsFilter implements ExceptionFilter {
catch(exception: HttpException | AnotherException, host: ArgumentsHost) {
// Lógica para manejar ambas excepciones
}
}
  • Algunas de las excepciones predefinidas más comunes son:
    • BadRequestException (400): Se utiliza cuando el cliente envía una solicitud inválida.
    • UnauthorizedException (401): Indica que el cliente no tiene la autenticación adecuada.
    • ForbiddenException (403): Indica que el cliente no tiene permiso para acceder a un recurso.
    • NotFoundException (404): Se utiliza cuando el recurso solicitado no existe.
    • InternalServerErrorException (500): Para errores internos del servidor.
  • Ejemplo de filtro global para todas las excepciones
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception instanceof HttpException
? exception.getStatus()
: 500;
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: (exception as any).message || 'Internal server error',
});
}
}

Diferencia entre DTO y Entity

EntityDTO
PropósitoRepresenta el modelo de datos de la base de datosDefine la estructura de los datos que recibe/envía la API
UsoCapa de persistenciaValidación y tipado de datos en la API
DecoradoresDecoradores del typeORM (@Entity, @Column)Decoradores de validación (@IsString, @IsNotEmpty)
AplicaciónDefine una tabla en la base de datosValida datos en una solicitud de creación o actualización
Ubicaciónquestion.entity.tscreate-question.dto.ts, update-question.dto.ts
  • Ahora en el caso que usemos Prisma no tiene tanto sentido el entity ya que las entidades no se definen en el código TypeScript directamente, sino en el archivo de esquema de Prisma (schema.prisma) y genera automáticamente el cliente de la base de datos y los tipos a partir de ese esquema