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
UnauthorizedExceptionen vez de devolverfalseda un mensaje claro al cliente. Devolverfalseproduce un403 Forbiddengené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 tokenAPP_GUARDen 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:
| Middleware | Guard | |
|---|---|---|
| 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 dependencias | Solo si se registra como clase en el módulo | ✅ Siempre — es un @Injectable() |
| Cuándo usarlo | Logging, parseo de headers, body transform | Autenticación, autorización, roles |
| Si bloquea | Llama a next() o no | Devuelve 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 #
| Concepto | Clave |
|---|---|
CanActivate | Interfaz que todo Guard implementa — un único método canActivate() |
canActivate() devuelve | boolean | Promise<boolean> | Observable<boolean> |
ExecutionContext | Da acceso al request, response y metadatos del handler destino |
switchToHttp().getRequest() | Extrae el Request de Express desde el contexto |
false → 403 automático | Devolver 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_GUARD | Registrar el Guard en un módulo para que tenga acceso a DI — mejor que useGlobalGuards() |
@SetMetadata() + Reflector | Pasar datos a los Guards desde los decoradores del Controller |
@Public() | Decorador para marcar rutas que el Guard global debe dejar pasar |
RolesGuard | Lee el rol del request.usuario (puesto por JwtAuthGuard) y compara con @Roles() |
| Guard vs Middleware | Guard tiene ExecutionContext y Reflector; Middleware solo tiene req/res/next |