# Guards, Interceptors, Filters y Pipes Avanzados en NestJS

## JwtAuthGuard con @Public() decorator

El patrón con Reflector permite marcar rutas públicas sin quitar el guard global.

```typescript
// common/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = (): MethodDecorator & ClassDecorator =>
  SetMetadata(IS_PUBLIC_KEY, true);

// common/guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private readonly reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;
    // La llamada a super activa la estrategia Passport
    return super.canActivate(context) as boolean | Promise<boolean>;
  }
}

// Registro global en app.module.ts o main.ts
app.useGlobalGuards(new JwtAuthGuard(reflector));

// Uso en controllers
@Public()
@Post('auth/login')
async login(@Body() dto: LoginDto): Promise<TokenResponseDto> {
  return this.authService.login(dto);
}
```

---

## RolesGuard con @Roles() decorator

```typescript
// common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export type AppRole = 'ADMIN' | 'EDITOR' | 'LECTOR';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: AppRole[]): MethodDecorator & ClassDecorator =>
  SetMetadata(ROLES_KEY, roles);

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

interface AuthenticatedRequest extends Request {
  user: { id: string; email: string; rol: AppRole };
}

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

  canActivate(context: ExecutionContext): boolean {
    const rolesRequeridos = this.reflector.getAllAndOverride<AppRole[]>(
      ROLES_KEY,
      [context.getHandler(), context.getClass()],
    );
    if (!rolesRequeridos || rolesRequeridos.length === 0) return true;

    const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const { user } = request;

    if (!rolesRequeridos.includes(user.rol)) {
      throw new ForbiddenException(
        `Se requiere uno de los roles: ${rolesRequeridos.join(', ')}`,
      );
    }
    return true;
  }
}

// Uso combinado en controller
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('ADMIN')
@Delete(':id')
async eliminar(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
  return this.usersService.eliminar(id);
}
```

---

## LoggingInterceptor con tiempo de ejecución

```typescript
// common/interceptors/logging.interceptor.ts
import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const request = context.switchToHttp().getRequest<{
      method: string;
      url: string;
    }>();
    const { method, url } = request;
    const inicio = Date.now();

    return next.handle().pipe(
      tap({
        next: () => {
          const duracionMs = Date.now() - inicio;
          this.logger.log(`${method} ${url} — ${duracionMs}ms`);
        },
        error: (error: unknown) => {
          const duracionMs = Date.now() - inicio;
          const mensaje = error instanceof Error ? error.message : 'Error desconocido';
          this.logger.error(`${method} ${url} — ${duracionMs}ms — ${mensaje}`);
        },
      }),
    );
  }
}
```

---

## TransformInterceptor — envolver responses en { data, metadata }

```typescript
// common/interceptors/transform.interceptor.ts
import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface ApiResponse<T> {
  data: T;
  timestamp: string;
  path: string;
}

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, ApiResponse<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler<T>,
  ): Observable<ApiResponse<T>> {
    const request = context.switchToHttp().getRequest<{ url: string }>();

    return next.handle().pipe(
      map((data) => ({
        data,
        timestamp: new Date().toISOString(),
        path: request.url,
      })),
    );
  }
}

// Registro global en main.ts
app.useGlobalInterceptors(new TransformInterceptor());
```

---

## HttpExceptionFilter — formato de error estandarizado

```typescript
// common/filters/http-exception.filter.ts
import {
  ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger,
} from '@nestjs/common';
import type { Request, Response } from 'express';

interface ErrorResponse {
  statusCode: number;
  timestamp: string;
  path: string;
  method: string;
  message: string | string[];
  error: string;
}

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

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

    let statusCode: number;
    let message: string | string[];
    let error: string;

    if (exception instanceof HttpException) {
      statusCode = exception.getStatus();
      const responseBody = exception.getResponse();
      if (typeof responseBody === 'object' && responseBody !== null) {
        const body = responseBody as Record<string, unknown>;
        message = (body['message'] as string | string[]) ?? exception.message;
        error = (body['error'] as string) ?? exception.name;
      } else {
        message = exception.message;
        error = exception.name;
      }
    } else {
      statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
      message = 'Error interno del servidor';
      error = 'InternalServerError';
      // Log completo solo para errores no esperados
      this.logger.error(
        `Error no controlado: ${exception instanceof Error ? exception.message : String(exception)}`,
        exception instanceof Error ? exception.stack : undefined,
      );
    }

    const errorResponse: ErrorResponse = {
      statusCode,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message,
      error,
    };

    response.status(statusCode).json(errorResponse);
  }
}
```

---

## Throttler guard — rate limiting

```typescript
// Instalación: npm install @nestjs/throttler
// app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';

@Module({
  imports: [
    ThrottlerModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ([{
        name: 'global',
        ttl: config.get<number>('THROTTLE_TTL_MS') ?? 60_000,
        limit: config.get<number>('THROTTLE_LIMIT') ?? 100,
      }]),
    }),
  ],
  providers: [
    // Guard global — aplica a todos los endpoints
    { provide: APP_GUARD, useClass: ThrottlerGuard },
  ],
})
export class AppModule {}

// Sobrescribir límite en un endpoint específico
import { Throttle, SkipThrottle } from '@nestjs/throttler';

@Throttle({ default: { ttl: 60_000, limit: 5 } })
@Post('auth/login')
async login(@Body() dto: LoginDto): Promise<TokenResponseDto> {
  return this.authService.login(dto);
}

@SkipThrottle()
@Get('health')
health(): { status: string } {
  return { status: 'ok' };
}
```

---

## ValidationPipe avanzado con mensajes personalizados

```typescript
// common/pipes/validation.pipe.ts
import { ValidationPipe, UnprocessableEntityException } from '@nestjs/common';
import { ValidationError } from 'class-validator';

type FieldErrors = Record<string, string[]>;

function extraerErrores(errores: ValidationError[], prefijo = ''): FieldErrors {
  return errores.reduce<FieldErrors>((acum, error) => {
    const campo = prefijo ? `${prefijo}.${error.property}` : error.property;

    if (error.constraints) {
      acum[campo] = Object.values(error.constraints);
    }
    if (error.children && error.children.length > 0) {
      Object.assign(acum, extraerErrores(error.children, campo));
    }
    return acum;
  }, {});
}

export const globalValidationPipe = new ValidationPipe({
  whitelist: true,
  forbidNonWhitelisted: true,
  transform: true,
  transformOptions: { enableImplicitConversion: true },
  exceptionFactory: (errores: ValidationError[]) => {
    const campos = extraerErrores(errores);
    return new UnprocessableEntityException({
      message: 'Error de validación',
      error: 'UnprocessableEntity',
      campos,
    });
  },
});
```
