↓Ir al contenido

Guards en NestJS

En clase construiste el manejo de errores con HttpException y filtros globales. El siguiente mecanismo del ciclo de vida de NestJS que necesitas dominar es el Guard — la capa que decide si una petición tiene permiso de llegar al Controller. Sin él, cualquier endpoint de tu aplicación es accesible por cualquiera.


1. Qué es un Guard #

Un Guard es una clase con una sola responsabilidad: responder true o false a la pregunta “¿esta petición puede continuar?”. Si devuelve true, la petición pasa al Controller. Si devuelve false, NestJS lanza automáticamente un 403 Forbidden.

Petición entrante
      │
      ▼
 Middleware         ← transforma o rechaza (sin acceso al contexto de NestJS)
      │
      ▼
  ┌─ Guard ─┐       ← ¿tiene permiso? true/false
  │ canActivate()│
  └─────────┘
      │ true
      ▼
 Interceptor (antes)
      │
      ▼
   Pipes            ← validación y transformación del input
      │
      ▼
  Controller        ← lógica del endpoint
      │
      ▼
 Interceptor (después)
      │
      ▼
  Exception Filter  ← manejo de errores (lo de hoy en clase)

Los Guards se ejecutan después del Middleware y antes de los Pipes. La diferencia clave con el Middleware es que los Guards tienen acceso al ExecutionContext — saben exactamente qué controlador y qué método van a ejecutar, lo que permite decisiones de autorización basadas en metadatos del decorador.


2. Anatomía de un Guard #

Todo Guard implementa la interfaz CanActivate y está decorado con @Injectable():

import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Observable } from "rxjs";

@Injectable()
export class MiGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // Extraer el Request de HTTP
    const request = context.switchToHttp().getRequest();

    // Lógica de autorización — devuelve true para permitir, false para denegar
    return !!request.headers.authorization;
  }
}

canActivate() puede ser síncrono (boolean), asíncrono (Promise<boolean>) u observable. En la práctica casi siempre es una Promise porque necesitas consultar la base de datos o verificar un JWT.

ExecutionContext — qué puedes extraer #

const context: ExecutionContext = ...; // recibido en canActivate()

// HTTP — el más común
const request  = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();

// Metadata del handler — útil para roles y permisos
const handler    = context.getHandler();   // la función del método del Controller
const controller = context.getClass();     // la clase del Controller

3. Guard de Autenticación JWT #

El Guard más común en cualquier aplicación con login. Extrae el token del header Authorization, lo verifica y adjunta el payload al request para que los Controllers lo usen.

// src/auth/guards/jwt-auth.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Request }    from "express";

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    const token   = this.extraerToken(request);

    if (!token) {
      throw new UnauthorizedException("Token requerido");
    }

    try {
      // verify() lanza si el token es inválido o expiró
      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET,
      });

      // Adjuntar el payload al request — disponible en el Controller
      request["usuario"] = payload;
      return true;

    } catch {
      throw new UnauthorizedException("Token inválido o expirado");
    }
  }

  private extraerToken(request: Request): string | undefined {
    // El header llega como "Bearer eyJ..."
    const [tipo, token] = request.headers.authorization?.split(" ") ?? [];
    return tipo === "Bearer" ? token : undefined;
  }
}

Lanzar UnauthorizedException en vez de devolver false da un mensaje claro al cliente. Devolver false produce un 403 Forbidden genérico sin cuerpo.

Acceder al usuario en el Controller #

// El payload que adjuntamos en el Guard
@Get("perfil")
@UseGuards(JwtAuthGuard)
obtenerPerfil(@Req() req: Request) {
  const usuario = req["usuario"]; // { sub: 1, email: "..." }
  return prisma.usuario.findUnique({ where: { id: usuario.sub } });
}

Para evitar usar strings mágicos como req["usuario"], puedes crear un decorador personalizado:

// src/auth/decorators/usuario-actual.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const UsuarioActual = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.usuario;
  },
);

// En el Controller — más limpio
@Get("perfil")
@UseGuards(JwtAuthGuard)
obtenerPerfil(@UsuarioActual() usuario: PayloadToken) {
  return prisma.usuario.findUnique({ where: { id: usuario.sub } });
}

4. Aplicar Guards #

A un método específico #

@Controller("pedidos")
export class PedidosController {

  @Get()
  @UseGuards(JwtAuthGuard)          // solo este endpoint está protegido
  listar() { ... }

  @Get("publicos")                   // este endpoint no requiere autenticación
  listarPublicos() { ... }
}

A todo un Controller #

@Controller("pedidos")
@UseGuards(JwtAuthGuard)            // todos los métodos de este Controller requieren JWT
export class PedidosController {
  @Get()    listar() { ... }
  @Post()   crear()  { ... }
  @Delete() borrar() { ... }
}

De forma global — todos los endpoints #

// main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new JwtAuthGuard());  // aplica a toda la aplicación

Limitación de useGlobalGuards(): el Guard registrado así se crea fuera del sistema de inyección de dependencias de NestJS. Si tu Guard necesita inyectar servicios (JwtService, PrismaService, etc.), usa el token APP_GUARD en un módulo en su lugar.

Global con inyección de dependencias — la forma correcta #

// app.module.ts
import { APP_GUARD } from "@nestjs/core";

@Module({
  providers: [
    {
      provide:  APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AppModule {}

Con APP_GUARD, NestJS instancia el Guard dentro de su contenedor de DI — JwtService, Reflector y cualquier otro servicio se inyectan normalmente.


5. Marcar Endpoints Públicos con @SetMetadata #

Cuando aplicas el JwtAuthGuard de forma global, todos los endpoints requieren token — incluyendo el de login, que es el único que no puede pedirlo. La solución estándar es un decorador @Public() que marca las rutas que el Guard debe dejar pasar.

Crear el decorador #

// src/auth/decorators/publico.decorator.ts
import { SetMetadata } from "@nestjs/common";

export const IS_PUBLIC_KEY = "isPublic";
export const Publico = () => SetMetadata(IS_PUBLIC_KEY, true);

@SetMetadata adjunta metadatos al handler o al controlador. El Guard los lee con Reflector.

Leer los metadatos en el Guard #

// src/auth/guards/jwt-auth.guard.ts
import { Reflector } from "@nestjs/core";
import { IS_PUBLIC_KEY } from "../decorators/publico.decorator";

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(
    private readonly jwtService:  JwtService,
    private readonly reflector:   Reflector,   // inyectado normalmente con APP_GUARD
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // ¿El endpoint está marcado como público?
    const esPublico = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),    // metadatos del método
      context.getClass(),      // metadatos del Controller
    ]);

    if (esPublico) return true;   // dejar pasar sin verificar el token

    // Si no es público, verificar el JWT normalmente
    const request = context.switchToHttp().getRequest();
    const token   = this.extraerToken(request);
    if (!token) throw new UnauthorizedException("Token requerido");

    try {
      const payload      = await this.jwtService.verifyAsync(token, { secret: process.env.JWT_SECRET });
      request["usuario"] = payload;
      return true;
    } catch {
      throw new UnauthorizedException("Token inválido o expirado");
    }
  }
}

Usar el decorador en el Controller #

@Controller("auth")
export class AuthController {

  @Publico()                        // este endpoint no requiere token
  @Post("login")
  login(@Body() dto: LoginDto) { ... }

  @Publico()                        // tampoco el registro
  @Post("registro")
  registro(@Body() dto: RegistroDto) { ... }
}

@Controller("usuarios")
@UseGuards(JwtAuthGuard)            // todos los demás requieren autenticación
export class UsuariosController { ... }

6. Guard de Roles — Autorización #

Autenticación (¿eres quien dices ser?) y autorización (¿tienes permiso para hacer esto?) son problemas distintos. El Guard de autenticación verifica el token; el Guard de roles verifica el rol del usuario dentro del token.

Decorador @Roles() #

// src/auth/decorators/roles.decorator.ts
import { SetMetadata } from "@nestjs/common";

export type Rol = "ADMIN" | "USUARIO" | "INSTRUCTOR";
export const ROLES_KEY = "roles";
export const Roles = (...roles: Rol[]) => SetMetadata(ROLES_KEY, roles);

RolesGuard #

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

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

  canActivate(context: ExecutionContext): boolean {
    // ¿Qué roles requiere este endpoint?
    const rolesRequeridos = this.reflector.getAllAndOverride<Rol[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // Si no tiene @Roles(), cualquier usuario autenticado puede acceder
    if (!rolesRequeridos || rolesRequeridos.length === 0) return true;

    // El usuario viene del JwtAuthGuard (que debe ejecutarse primero)
    const { usuario } = context.switchToHttp().getRequest();

    const tienePermiso = rolesRequeridos.includes(usuario?.rol);
    if (!tienePermiso) {
      throw new ForbiddenException("No tienes permiso para esta acción");
    }
    return true;
  }
}

Usar ambos Guards juntos #

@Controller("cursos")
@UseGuards(JwtAuthGuard, RolesGuard)   // el orden importa — auth primero, roles después
export class CursosController {

  @Get()
  listar() { ... }                      // cualquier usuario autenticado

  @Post()
  @Roles("ADMIN", "INSTRUCTOR")         // solo ADMIN e INSTRUCTOR
  crear(@Body() dto: CrearCursoDto) { ... }

  @Delete(":id")
  @Roles("ADMIN")                       // solo ADMIN
  eliminar(@Param("id") id: string) { ... }
}

El RolesGuard asume que JwtAuthGuard ya adjuntó request.usuario con el campo rol. Si los Guards están globales vía APP_GUARD, registra el JwtAuthGuard primero.


7. Guard vs Middleware #

Ambos se ejecutan antes del Controller, pero tienen capacidades distintas:

MiddlewareGuard
Acceso al ExecutionContext❌ No — solo tiene req, res, next✅ Sí — sabe qué Controller y método van a ejecutar
Acceso a metadatos de decoradores❌ No✅ Sí — via Reflector
Inyección de dependenciasSolo si se registra como clase en el módulo✅ Siempre — es un @Injectable()
Cuándo usarloLogging, parseo de headers, body transformAutenticación, autorización, roles
Si bloqueaLlama a next() o noDevuelve true/false o lanza excepción

La regla práctica: si necesitas saber qué ruta o qué decoradores tiene el endpoint para tomar la decisión, usa un Guard. Si solo necesitas operar sobre el request sin importar el destino, usa Middleware.


Preguntas Frecuentes #

¿Qué pasa si canActivate() devuelve false en vez de lanzar una excepción? #

NestJS lanza automáticamente un 403 Forbidden con cuerpo vacío. Lanzar UnauthorizedException o ForbiddenException explícitamente da más control sobre el mensaje de error — y los ExceptionFilters que configuraste hoy en clase los procesan igual que cualquier otra excepción.

¿Por qué JwtAuthGuard debe ejecutarse antes que RolesGuard? #

Porque RolesGuard lee request.usuario.rol — ese campo lo adjunta JwtAuthGuard. Si RolesGuard corre primero, request.usuario es undefined y el chequeo de rol siempre falla. El orden en @UseGuards(A, B) es el orden de ejecución: A primero, luego B.

¿Puedo usar Guards en WebSockets o microservicios? #

Sí — por eso ExecutionContext tiene métodos como switchToHttp(), switchToWs() y switchToRpc(). Un Guard para WebSockets usaría context.switchToWs().getData() en vez de switchToHttp().getRequest(). La interfaz CanActivate es la misma.

¿Cuál es la diferencia entre Reflector.get() y Reflector.getAllAndOverride()? #

  • get(key, target) — lee metadatos de un solo target (handler o clase).
  • getAllAndOverride(key, [handler, clase]) — busca en el handler primero; si no hay metadatos ahí, busca en la clase. Devuelve el primer valor encontrado. Es el método correcto cuando un decorador puede estar en el método o en el Controller.
  • getAllAndMerge(key, [handler, clase]) — combina los metadatos de ambos (útil si los roles se acumulan en varios niveles).

¿Puedo usar el Guard para adjuntar datos al request además de autorizar? #

Sí — de hecho es la práctica estándar. El JwtAuthGuard adjunta el payload del token a request.usuario. También podrías hacer una consulta a la base de datos para adjuntar el objeto Usuario completo, aunque eso añade un query por petición. La alternativa es hacer el query solo en los Controllers que lo necesitan.


Resumen #

ConceptoClave
CanActivateInterfaz que todo Guard implementa — un único método canActivate()
canActivate() devuelveboolean | Promise<boolean> | Observable<boolean>
ExecutionContextDa acceso al request, response y metadatos del handler destino
switchToHttp().getRequest()Extrae el Request de Express desde el contexto
false → 403 automáticoDevolver false lanza ForbiddenException; lanzar la excepción da más control
@UseGuards()En método, Controller o global — el orden de argumentos es el orden de ejecución
APP_GUARDRegistrar el Guard en un módulo para que tenga acceso a DI — mejor que useGlobalGuards()
@SetMetadata() + ReflectorPasar datos a los Guards desde los decoradores del Controller
@Public()Decorador para marcar rutas que el Guard global debe dejar pasar
RolesGuardLee el rol del request.usuario (puesto por JwtAuthGuard) y compara con @Roles()
Guard vs MiddlewareGuard tiene ExecutionContext y Reflector; Middleware solo tiene req/res/next