Saltar al contenido principal

Manejo de Datos y Capas Adicionales

7. Regla sobre Configuración y Variables de Entorno

Regla Principal

Regla: La configuración de la aplicación (ej. credenciales de base de datos, claves API, puertos) debe gestionarse utilizando el módulo @nestjs/config.

  1. Las variables de entorno deben definirse en un archivo .env en la raíz del proyecto (y este archivo debe estar en .gitignore).
  2. Se debe crear un esquema de validación (usando Joi o class-validator) para asegurar que todas las variables de entorno requeridas estén presentes y tengan el formato correcto al iniciar la aplicación.
  3. La configuración validada debe cargarse usando ConfigModule.forRoot() en AppModule, especificando isGlobal: true para que ConfigService esté disponible en toda la aplicación sin necesidad de importar ConfigModule en cada módulo.
  4. El acceso a las variables de configuración debe realizarse exclusivamente a través de ConfigService inyectado en los servicios o providers que lo necesiten. Está prohibido acceder directamente a process.env fuera del módulo de configuración.

Contexto y Justificación

Separar la configuración del código es fundamental (principio de Twelve-Factor App). @nestjs/config proporciona una solución robusta y estandarizada para cargar, validar y acceder a variables de entorno, mejorando la seguridad (evitando hardcodear secretos), la portabilidad entre entornos (desarrollo, producción) y la mantenibilidad.

Ejemplos y Contraejemplos

  • Correcto:
    // --- .env (No subir a Git) ---
    DATABASE_URL=postgresql://user:password@localhost:5432/mydb
    PORT=3001
    JWT_SECRET=a-very-strong-secret-key
    API_KEY_EXTERNAL_SERVICE=xyz789

    // --- src/config/validation.schema.ts (usando Joi) ---
    import * as Joi from 'joi';

    export const validationSchema = Joi.object({
    DATABASE_URL: Joi.string().uri().required(),
    PORT: Joi.number().default(3000),
    JWT_SECRET: Joi.string().min(32).required(),
    API_KEY_EXTERNAL_SERVICE: Joi.string().required(),
    });

    // --- src/app.module.ts ---
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config';
    import { validationSchema } from './config/validation.schema';
    // ... otros imports ...

    @Module({
    imports: [
    ConfigModule.forRoot({
    isGlobal: true, // Hace ConfigService disponible globalmente
    envFilePath: '.env', // Especifica el archivo .env
    validationSchema, // Aplica el esquema de validación
    validationOptions: {
    allowUnknown: true, // Permite otras variables no definidas en el schema
    abortEarly: false, // Muestra todos los errores de validación
    },
    }),
    // ... otros módulos ...
    ],
    // ... controllers, providers ...
    })
    export class AppModule {}

    // --- src/modules/database/database.service.ts ---
    import { Injectable } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';

    @Injectable()
    export class DatabaseService {
    private connectionString: string;
    constructor(private configService: ConfigService) {
    // Accede a la variable validada a través de ConfigService
    this.connectionString = this.configService.get<string>('DATABASE_URL');
    console.log('Conectando a:', this.connectionString); // ¡Solo para demo!
    }
    // ... métodos del servicio ...
    }
  • Incorrecto:
    // ¡INCORRECTO: Hardcodear secretos!
    const dbUrl = 'postgresql://user:password@localhost:5432/mydb';

    // ¡INCORRECTO: Acceder directamente a process.env fuera de config!
    import { Injectable } from '@nestjs/common';
    @Injectable()
    export class BadService {
    private secret = process.env.JWT_SECRET; // Propenso a errores si no está definida o validada
    // ...
    }

    // Ausencia de ConfigModule o validación en AppModule.

Cuándo Aplicar

Siempre, para cualquier valor que pueda cambiar entre entornos o que sea sensible.

Cuándo Evitar o Flexibilizar

Nunca se debe hardcodear configuración sensible o específica del entorno. Para constantes verdaderamente fijas y no sensibles, se pueden usar constantes en TypeScript, pero la configuración externa debe usar @nestjs/config.

8. Regla sobre Manejo de Datos con ORM (TypeORM/Prisma)

Regla Principal

Regla: El acceso a la base de datos debe realizarse a través de un ORM (Object-Relational Mapper) como TypeORM o Prisma, siguiendo las mejores prácticas asociadas.

  1. Módulo Dedicado: La configuración y conexión del ORM deben encapsularse en un módulo dedicado (ej. DatabaseModule), que luego se importa en AppModule o módulos específicos.
  2. Patrón Repositorio (Opcional pero Recomendado): Abstraer las consultas específicas del ORM detrás de una interfaz de repositorio (IUserRepository) e implementar esa interfaz con una clase que utilice el ORM (PrismaUserRepository). Los servicios deben depender de la interfaz del repositorio (Inversión de Dependencias), no directamente del ORM o sus modelos generados.
  3. Entidades/Modelos: Definir entidades (TypeORM) o modelos (Prisma) que representen las tablas de la base de datos. Usar decoradores/schema para definir relaciones, tipos de datos y restricciones.
  4. Migraciones: Utilizar las herramientas de migración del ORM para gestionar los cambios en el esquema de la base de datos de forma controlada y versionada.
  5. Transacciones: Utilizar transacciones explícitas del ORM para operaciones que involucren múltiples escrituras y requieran atomicidad.

Contexto y Justificación

Los ORMs simplifican la interacción con la base de datos, proporcionan seguridad contra inyección SQL (si se usan correctamente) y facilitan el mapeo entre objetos de la aplicación y registros de la base de datos. El patrón Repositorio añade una capa de abstracción que desacopla la lógica de negocio del ORM específico, facilitando el testing y la posibilidad de cambiar de ORM o fuente de datos en el futuro. Las migraciones son esenciales para la evolución segura del esquema.

Ejemplos y Contraejemplos

  • Correcto (con Prisma y Repositorio):
    // --- prisma/schema.prisma ---
    model User {
    id String @id @default(uuid())
    email String @unique
    name String?
    password String
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    }

    // --- src/modules/users/interfaces/user.repository.interface.ts ---
    import { User } from '@prisma/client';
    import { CreateUserPersistenceDto } from '../dtos/persistence.dto'; // DTO específico para persistencia

    export interface IUserRepository {
    findById(id: string): Promise<User | null>;
    findByEmail(email: string): Promise<User | null>;
    create(data: CreateUserPersistenceDto): Promise<User>;
    }
    export const USER_REPOSITORY_TOKEN = Symbol('IUserRepository');

    // --- src/modules/users/repositories/prisma-user.repository.ts ---
    import { Injectable } from '@nestjs/common';
    import { PrismaService } from '@app/database/prisma.service'; // Servicio Prisma centralizado
    import { User } from '@prisma/client';
    import { IUserRepository } from '../interfaces/user.repository.interface';
    import { CreateUserPersistenceDto } from '../dtos/persistence.dto';

    @Injectable()
    export class PrismaUserRepository implements IUserRepository {
    constructor(private readonly prisma: PrismaService) {}

    async findById(id: string): Promise<User | null> {
    return this.prisma.user.findUnique({ where: { id } });
    }

    async findByEmail(email: string): Promise<User | null> {
    return this.prisma.user.findUnique({ where: { email } });
    }

    async create(data: CreateUserPersistenceDto): Promise<User> {
    return this.prisma.user.create({ data });
    }
    }

    // --- src/modules/users/services/users.service.ts ---
    import { Injectable, Inject, NotFoundException } from '@nestjs/common';
    import { IUserRepository, USER_REPOSITORY_TOKEN } from '../interfaces/user.repository.interface';
    // ... otros imports ...

    @Injectable()
    export class UsersService {
    constructor(
    // Depende de la interfaz del repositorio
    @Inject(USER_REPOSITORY_TOKEN) private readonly userRepository: IUserRepository
    ) {}

    async getUser(id: string): Promise<User> {
    const user = await this.userRepository.findById(id);
    if (!user) throw new NotFoundException();
    return user;
    }
    // ... otros métodos usando userRepository ...
    }

    // --- src/modules/users/users.module.ts ---
    import { Module } from '@nestjs/common';
    import { PrismaUserRepository } from './repositories/prisma-user.repository';
    import { USER_REPOSITORY_TOKEN } from './interfaces/user.repository.interface';
    // ... otros imports ...

    @Module({
    // ... controllers, imports ...
    providers: [
    UsersService,
    {
    provide: USER_REPOSITORY_TOKEN, // Proveedor para la interfaz
    useClass: PrismaUserRepository, // Usa la implementación concreta
    },
    ],
    exports: [UsersService],
    })
    export class UsersModule {}

    // --- src/database/prisma.service.ts (Ejemplo básico) ---
    import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
    import { PrismaClient } from '@prisma/client';

    @Injectable()
    export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
    async onModuleInit() {
    await this.$connect();
    }
    async onModuleDestroy() {
    await this.$disconnect();
    }
    }

    // --- src/database/database.module.ts ---
    import { Module, Global } from '@nestjs/common';
    import { PrismaService } from './prisma.service';

    @Global() // Hace PrismaService disponible globalmente
    @Module({
    providers: [PrismaService],
    exports: [PrismaService],
    })
    export class DatabaseModule {}
  • Incorrecto: Servicios interactuando directamente con PrismaClient (sin repositorio). Construcción manual de queries SQL en strings (vulnerable a inyección). No usar migraciones. Lógica de negocio compleja dentro de la clase del repositorio.

Cuándo Aplicar

Siempre que se interactúe con una base de datos relacional o NoSQL. El patrón Repositorio es altamente recomendado para aplicaciones de medianas a grandes.

Cuándo Evitar o Flexibilizar

El patrón Repositorio puede ser opcional para microservicios muy pequeños o prototipos rápidos, pero el uso del ORM y sus herramientas (migraciones) sigue siendo obligatorio. Si no se usa el repositorio, el servicio interactuará directamente con el cliente del ORM (ej. PrismaService).

9. Regla sobre Guards

Regla Principal

Regla: La lógica de autorización (verificar si un usuario tiene permiso para acceder a un recurso/ruta) debe implementarse utilizando Guards (@Injectable que implementa CanActivate).

  1. Los Guards deben tener una única responsabilidad (ej. JwtAuthGuard para verificar token JWT, RolesGuard para verificar roles de usuario).
  2. Deben retornar true (o Promise<true> / Observable<true>) si el acceso es permitido, o false (o Promise<false> / Observable<false>) si es denegado. También pueden lanzar una excepción (ej. ForbiddenException) para denegar el acceso con un error específico.
  3. Deben obtener información del usuario (ej. desde el token JWT decodificado) a través del objeto ExecutionContext y su método switchToHttp().getRequest().
  4. Pueden aplicarse a nivel de controlador (@UseGuards(MyGuard) en la clase) o a nivel de método/ruta (@UseGuards(MyGuard) en el método).
  5. Guards comunes y reutilizables deben ubicarse en src/common/guards.

Contexto y Justificación

Los Guards proporcionan un mecanismo declarativo y reutilizable para separar la lógica de autorización de los controladores y servicios. Esto mejora la legibilidad, mantenibilidad y testeabilidad, adhiriéndose al principio de Responsabilidad Única. NestJS ejecuta los Guards antes que los Pipes, Interceptors y el propio handler de la ruta.

Ejemplos y Contraejemplos

  • Correcto (RolesGuard):
    // --- src/common/decorators/roles.decorator.ts ---
    import { SetMetadata } from '@nestjs/common';
    export const ROLES_KEY = 'roles';
    export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

    // --- src/common/guards/roles.guard.ts ---
    import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
    import { Reflector } from '@nestjs/core';
    import { ROLES_KEY } from '../decorators/roles.decorator';

    @Injectable()
    export class RolesGuard implements CanActivate {
    constructor(private reflector: Reflector) {}

    canActivate(context: ExecutionContext): boolean {
    // Obtiene los roles requeridos definidos con @Roles(...) en la ruta/controlador
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
    context.getHandler(),
    context.getClass(),
    ]);
    if (!requiredRoles) {
    return true; // Si no se definieron roles, permite el acceso
    }

    // Obtiene el usuario del request (asumiendo que JwtAuthGuard ya lo añadió)
    const { user } = context.switchToHttp().getRequest();

    if (!user || !user.roles) {
    throw new ForbiddenException('No tienes permisos para acceder a este recurso.');
    }

    // Verifica si el usuario tiene al menos uno de los roles requeridos
    const hasRole = requiredRoles.some((role) => user.roles?.includes(role));

    if (!hasRole) {
    throw new ForbiddenException('No tienes los roles necesarios.');
    }

    return true;
    }
    }

    // --- En el controlador ---
    import { Controller, Get, UseGuards } from '@nestjs/common';
    import { JwtAuthGuard } from './guards/jwt-auth.guard'; // Asume que este guard verifica y añade el user al request
    import { RolesGuard } from './guards/roles.guard';
    import { Roles } from './decorators/roles.decorator';

    @Controller('admin')
    // Aplica primero el guard JWT, luego el guard de Roles
    @UseGuards(JwtAuthGuard, RolesGuard)
    export class AdminController {
    @Get('dashboard')
    @Roles('admin', 'superadmin') // Solo usuarios con rol 'admin' o 'superadmin' pueden acceder
    getDashboard() {
    return { message: 'Welcome to the Admin Dashboard!' };
    }

    @Get('users')
    @Roles('superadmin') // Solo 'superadmin'
    getUsers() {
    // ...
    }
    }
  • Incorrecto: Lógica de verificación de roles o permisos dentro de los métodos del controlador o servicio. Guards realizando tareas que no son de autorización (ej. transformación de datos).

Cuándo Aplicar

Siempre que se necesite controlar el acceso a rutas basado en roles, permisos, propiedad de recursos u otras condiciones de autorización.

Cuándo Evitar o Flexibilizar

No usar Guards para lógica de autenticación (verificar identidad del usuario, eso suele hacerse en Middleware o Passport strategies) ni para validación/transformación de datos (usar Pipes).

10. Regla sobre Pipes

Regla Principal

Regla: La validación y transformación de los datos de entrada de una petición (parámetros de ruta, query params, body) debe realizarse utilizando Pipes (@Injectable que implementa PipeTransform).

  1. NestJS proporciona Pipes incorporados útiles (ValidationPipe, ParseIntPipe, ParseUUIDPipe, ParseEnumPipe, DefaultValuePipe) que deben usarse preferentemente.
  2. Para lógica de validación o transformación personalizada y reutilizable, se deben crear Pipes personalizados.
  3. Un Pipe debe recibir el valor de entrada y el metadata (tipo de parámetro, tipo de dato esperado) y debe retornar el valor transformado o lanzar una excepción (ej. BadRequestException) si la validación falla.
  4. El ValidationPipe debe usarse globalmente (ver Norma 6) para la validación basada en DTOs con class-validator.
  5. Pipes específicos pueden aplicarse a nivel de parámetro (@Param('id', ParseIntPipe), @Body(MyCustomPipe)).
  6. Pipes personalizados reutilizables deben ubicarse en src/common/pipes.

Contexto y Justificación

Los Pipes permiten procesar los datos de entrada antes de que lleguen al handler de la ruta. Centralizan la lógica de validación y transformación, manteniendo los controladores limpios y asegurando que los datos recibidos por los servicios sean válidos y tengan el formato esperado. Son ejecutados después de los Guards y Middleware, pero antes de los Interceptors y el handler.

Ejemplos y Contraejemplos

  • Correcto (Uso de Pipes incorporados y Global):
    // --- src/main.ts ---
    // Ver Norma 6 para la configuración de ValidationPipe global

    // --- En el controlador ---
    import {
    Controller, Get, Param, Query, Body, DefaultValuePipe, ParseIntPipe, ParseUUIDPipe, UsePipes, ValidationPipe
    } from '@nestjs/common';
    import { CreateItemDto } from './dtos/create-item.dto'; // DTO con class-validator

    @Controller('items')
    export class ItemsController {
    @Get(':id')
    // ParseUUIDPipe valida que 'id' sea un UUID
    findOne(@Param('id', ParseUUIDPipe) id: string) {
    console.log(typeof id); // string
    // ...
    }

    @Get()
    findAll(
    // DefaultValuePipe asigna 10 si 'limit' no se provee
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
    // ParseIntPipe convierte y valida que 'offset' sea un entero
    @Query('offset', new DefaultValuePipe(0), ParseIntPipe) offset: number,
    ) {
    console.log(typeof limit, typeof offset); // number number
    // ...
    }

    @Post()
    // ValidationPipe (aplicado globalmente o aquí) valida el DTO
    // @UsePipes(new ValidationPipe({ whitelist: true })) // Si no es global
    create(@Body() createItemDto: CreateItemDto) {
    // createItemDto está validado
    // ...
    }
    }
  • Incorrecto: Validación manual de tipos o formatos dentro del controlador (if (isNaN(parseInt(id))) throw...). Transformación manual de datos dentro del controlador. No usar ValidationPipe para DTOs.

Cuándo Aplicar

Siempre para validar y/o transformar parámetros de ruta, query params o el body de la petición.

Cuándo Evitar o Flexibilizar

No usar Pipes para lógica de autorización (usar Guards) o para modificar la respuesta (usar Interceptors).

11. Regla sobre Interceptors

Regla Principal

Regla: La lógica que necesita ejecutarse antes y/o después del handler de la ruta, o que necesita modificar/transformar el resultado antes de enviarlo al cliente, debe implementarse utilizando Interceptors (@Injectable que implementa NestInterceptor).

  1. Los Interceptors deben usarse para tareas transversales como: logging de peticiones/respuestas, transformación de la estructura de respuesta, caching de respuestas, manejo de timeouts, serialización de datos salientes (ej. excluir propiedades).
  2. Un Interceptor tiene acceso al ExecutionContext (como los Guards) y al CallHandler, que permite interactuar con el flujo de la respuesta (a través de handle() que devuelve un Observable).
  3. Se deben usar operadores de RxJS (como map, tap, catchError) dentro del método intercept para manipular el stream de respuesta.
  4. Pueden aplicarse globalmente (app.useGlobalInterceptors), a nivel de controlador (@UseInterceptors) o a nivel de método.
  5. Interceptores reutilizables deben ubicarse en src/common/interceptors.
  6. El ClassSerializerInterceptor debe usarse globalmente si se utiliza class-transformer para controlar la serialización de entidades/DTOs salientes (ej. con @Exclude, @Expose).

Contexto y Justificación

Los Interceptors proporcionan un potente mecanismo AOP (Aspect-Oriented Programming) para añadir comportamiento alrededor de la ejecución de los handlers. Permiten mantener los controladores y servicios enfocados en su lógica principal, mientras que tareas transversales como el formateo de respuestas o el logging se manejan de forma separada y reutilizable.

Ejemplos y Contraejemplos

  • Correcto (Transformación de Respuesta y Serialización):
    // --- src/common/interceptors/transform.interceptor.ts ---
    import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';

    export interface Response<T> {
    statusCode: number;
    message: string;
    data: T;
    }

    @Injectable()
    export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
    intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    const statusCode = context.switchToHttp().getResponse().statusCode;
    return next.handle().pipe(
    map(data => ({
    statusCode: statusCode,
    message: 'Success', // Podría ser más dinámico
    data: data, // Envuelve la data original
    })),
    );
    }
    }

    // --- src/modules/users/entities/user.entity.ts (Ejemplo con Exclude) ---
    import { Exclude } from 'class-transformer';

    export class User {
    id: string;
    email: string;
    name: string;

    @Exclude() // Excluye la contraseña de la respuesta JSON
    passwordHash: string;

    constructor(partial: Partial<User>) {
    Object.assign(this, partial);
    }
    }

    // --- src/main.ts ---
    import { NestFactory, Reflector } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
    import { TransformInterceptor } from './common/interceptors/transform.interceptor';

    async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    app.useGlobalPipes(new ValidationPipe(/* ... */));

    // Aplica ClassSerializerInterceptor para @Exclude/@Expose
    app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
    // Aplica el interceptor de transformación de respuesta globalmente
    app.useGlobalInterceptors(new TransformInterceptor());

    await app.listen(3000);
    }
    bootstrap();

    // --- En el controlador (El handler devuelve la entidad User) ---
    import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';
    import { UsersService } from './services/users.service';
    import { User } from './entities/user.entity';

    @Controller('users')
    export class UsersController {
    constructor(private readonly usersService: UsersService) {}

    @Get(':id')
    async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
    // El servicio devuelve una instancia de User (con passwordHash)
    return this.usersService.findUserById(id);
    }
    }
    // La respuesta final será:
    // {
    // "statusCode": 200,
    // "message": "Success",
    // "data": {
    // "id": "...",
    // "email": "...",
    // "name": "..."
    // // passwordHash está excluido gracias a ClassSerializerInterceptor y @Exclude
    // }
    // }
  • Incorrecto: Formatear manualmente la estructura de respuesta dentro de cada método del controlador. Realizar logging extenso dentro de los servicios. Usar interceptors para validación (usar Pipes) o autorización (usar Guards).

Cuándo Aplicar

Para logging, transformación/estandarización de respuestas, caching, serialización controlada de datos salientes, manejo de timeouts.

Cuándo Evitar o Flexibilizar

No usar para lógica de negocio principal, validación de entrada o autorización.

12. Regla sobre Filtros de Excepción

Regla Principal

Regla: El manejo centralizado de excepciones no controladas y la personalización del formato de respuesta de errores deben realizarse utilizando Filtros de Excepción (@Catch decorador en una clase que implementa ExceptionFilter).

  1. NestJS tiene un filtro de excepciones global base que maneja excepciones de tipo HttpException y cualquier otra excepción no controlada (resultando en un 500 Internal Server Error genérico).
  2. Se deben crear filtros personalizados para: capturar tipos específicos de excepciones (incluyendo errores que no heredan de HttpException), loggear errores de forma centralizada, y/o estandarizar el formato de la respuesta de error JSON.
  3. Un filtro personalizado debe implementar la interfaz ExceptionFilter y su método catch(exception: T, host: ArgumentsHost). Dentro de catch, se obtiene acceso a los objetos request y response para enviar una respuesta personalizada.
  4. Los filtros pueden capturar una o más tipos de excepciones específicas (@Catch(MyException, YourException)) o todas las excepciones (@Catch()).
  5. Pueden aplicarse globalmente (app.useGlobalFilters), a nivel de controlador (@UseFilters) o a nivel de método.
  6. Filtros reutilizables deben ubicarse en src/common/filters.

Contexto y Justificación

Los Filtros de Excepción proporcionan un lugar único para manejar los errores que ocurren durante el ciclo de vida de la petición. Permiten separar la lógica de manejo de errores del código principal (controladores, servicios), estandarizar las respuestas de error que ve el cliente y realizar logging de errores de forma consistente.

Ejemplos y Contraejemplos

  • Correcto (Filtro Global para Errores HTTP y Otros):
    // --- src/common/filters/all-exceptions.filter.ts ---
    import {
    ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
    } from '@nestjs/common';
    import { HttpAdapterHost } from '@nestjs/core'; // Para compatibilidad platform-agnostic
    import { Request, Response } from 'express'; // O importar de fastify si se usa

    @Catch() // Captura todas las excepciones
    export class AllExceptionsFilter implements ExceptionFilter {
    constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

    catch(exception: unknown, host: ArgumentsHost): void {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const httpStatus =
    exception instanceof HttpException
    ? exception.getStatus()
    : HttpStatus.INTERNAL_SERVER_ERROR;

    let errorMessage = 'Internal server error';
    let errorDetails: any = undefined;

    if (exception instanceof HttpException) {
    const errorResponse = exception.getResponse();
    errorMessage = typeof errorResponse === 'string' ? errorResponse : (errorResponse as any).message || exception.message;
    if (typeof errorResponse === 'object' && (errorResponse as any).error) {
    errorDetails = (errorResponse as any).error;
    }
    } else if (exception instanceof Error) {
    // Podrías querer loggear el stack trace aquí para errores no-HTTP
    console.error('[AllExceptionsFilter] Non-HTTP Error:', exception);
    errorMessage = exception.message || errorMessage;
    // No exponer stack trace en producción
    // errorDetails = process.env.NODE_ENV !== 'production' ? exception.stack : undefined;
    }

    const responseBody = {
    statusCode: httpStatus,
    timestamp: new Date().toISOString(),
    path: httpAdapter.getRequestUrl(request),
    method: httpAdapter.getRequestMethod(request),
    message: errorMessage,
    ...(errorDetails && { details: errorDetails }), // Añade detalles si existen
    };

    // Loggear el error aquí si es necesario (usando un LoggerService inyectado)
    // this.logger.error(`[${request.method}] ${request.url} - Status: ${httpStatus}`, exception.stack);

    httpAdapter.reply(response, responseBody, httpStatus);
    }
    }

    // --- src/main.ts ---
    import { HttpAdapterHost, NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
    // ... otros imports

    async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    // ... otros app.use ...

    const httpAdapterHost = app.get(HttpAdapterHost);
    app.useGlobalFilters(new AllExceptionsFilter(httpAdapterHost));

    await app.listen(3000);
    }
    bootstrap();
  • Incorrecto: Bloques try/catch masivos en los controladores para formatear errores. Devolver mensajes de error inconsistentes desde diferentes partes de la aplicación. No manejar explícitamente excepciones específicas del dominio si requieren un tratamiento particular.

Cuándo Aplicar

Siempre se debe tener al menos el filtro base de NestJS. Crear filtros personalizados cuando se necesite estandarizar el formato de error, loggear errores centralizadamente o manejar tipos específicos de excepciones de forma diferente.

Cuándo Evitar o Flexibilizar

Para aplicaciones muy simples, el filtro base puede ser suficiente si no se requiere un formato de error específico o logging centralizado. Sin embargo, un filtro global personalizado es una buena práctica general.