Clase 20 - Middleware y Nest
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.

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.

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 comocontrollers: 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étodouse()
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 interfazNestModule. 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 guardias, filtros 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
PipeTransformy se encarga de dos tareas principales:- 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.
- 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:
- ValidationPipe: Valida los datos basados en clases DTO y decoradores de validación.
- ParseIntPipe: Convierte los parámetros de la solicitud a enteros.
- ParseBoolPipe: Convierte valores a booleanos.
- 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étodocanActivate(), que debe devolver un valor booleano o una promesa que resuelva un booleano. Si el guard devuelvetrue, la solicitud puede continuar hacia el controlador; si devuelvefalse, 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
NestInterceptory contiene un métodointercept(). Este método recibe dos parámetros: el contexto de ejecución (ExecutionContext) y elCallHandler, 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:
- Transformación de respuestas: Modificar la estructura de la respuesta antes de enviarla al cliente, por ejemplo, eliminando datos sensibles o agregando metadatos.
- Manejo de excepciones: Capturar y modificar las excepciones antes de que se devuelvan como respuestas, o transformar las excepciones de una forma personalizada.
- Registro de tiempos de ejecución: Medir el tiempo que toma una operación dentro del controlador y registrar esta información para monitoreo.
- 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:- La excepción lanzada (por ejemplo, un error HTTP o un error personalizado).
- 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 HttpExceptionexport 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
| Entity | DTO | |
|---|---|---|
| Propósito | Representa el modelo de datos de la base de datos | Define la estructura de los datos que recibe/envía la API |
| Uso | Capa de persistencia | Validación y tipado de datos en la API |
| Decoradores | Decoradores del typeORM (@Entity, @Column) | Decoradores de validación (@IsString, @IsNotEmpty) |
| Aplicación | Define una tabla en la base de datos | Valida datos en una solicitud de creación o actualización |
| Ubicación | question.entity.ts | create-question.dto.ts, update-question.dto.ts |
- Ahora en el caso que usemos Prisma no tiene tanto sentido el
entityya 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