Ángel David Roque Ayala
Andres Trujillo
Gracias por las notas!
Controlar el acceso a los endpoints de una aplicación es fundamental, y en NestJS los guardianes ofrecen una forma elegante de hacerlo. Pero, ¿qué sucede cuando necesitas que ciertos endpoints sean públicos mientras el resto permanece protegido? La respuesta está en combinar guardianes con metadata y decoradores personalizados, logrando un código más flexible y mantenible.
¿Cómo proteger todos los endpoints de un controlador con un guardián?
Cuando se mueve el decorador @UseGuards() al nivel de la clase completa, todos los endpoints dentro de ese controlador quedan protegidos automáticamente [01:00]. Esto significa que cada ruta, ya sea getHello, newEndpoint o cualquier otra, requerirá cumplir con la validación definida en el guardián, por ejemplo, enviar un header de autorización con la API key correcta.
Sin embargo, en la práctica no siempre queremos que absolutamente todos los endpoints exijan autenticación. Algunos necesitan ser de acceso libre, y ahí es donde entra en juego la metadata.
¿Qué es setMetadata y cómo permite crear endpoints públicos?
El decorador setMetadata se importa desde @nestjs/common [02:25] y permite inyectar información adicional al contexto de un endpoint. Se usa de la siguiente manera:
isPublic.true.De esta forma, aunque el guardián se aplique a todo el controlador, los endpoints marcados con @SetMetadata('isPublic', true) podrán ser identificados como públicos.
¿Cómo lee el guardián la metadata del endpoint?
Para que el guardián pueda leer la metadata, se utiliza la clase Reflector, que se importa desde @nestjs/core [03:32]. Este servicio se inyecta en el constructor del guardián mediante inyección de dependencias:
typescript constructor(private reflector: Reflector) {}
Dentro del método canActivate, se obtiene el valor de la metadata así [04:05]:
typescript const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler()); if (isPublic) { return true; }
isPublic es true, el guardián retorna true directamente sin validar el header, permitiendo el acceso libre.false, se ejecuta la lógica habitual de validación del token.Es importante que el nombre de la metadata coincida exactamente con el que se definió en el decorador; de lo contrario, el Reflector no podrá obtenerla.
¿Cómo se comportan las rutas públicas y protegidas?
Al probar en Insomnia [05:15], el resultado es claro:
¿Por qué crear un decorador personalizado para rutas públicas?
Usar @SetMetadata('isPublic', true) directamente tiene un riesgo: escribir mal el nombre de la metadata en algún punto del código. Para evitar este problema y hacer el código más mantenible, se recomienda crear un decorador personalizado [07:05].
Se crea un archivo dentro de una carpeta decorators en el módulo de autenticación, por ejemplo public.decorator.ts:
typescript import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
IS_PUBLIC_KEY centraliza el nombre de la metadata en un solo lugar.@Public() encapsula la lógica de SetMetadata con el valor true por defecto.En el guardián, se importa IS_PUBLIC_KEY desde el mismo archivo [09:10], reemplazando el string literal:
typescript const isPublic = this.reflector.get<boolean>(IS_PUBLIC_KEY, context.getHandler());
Ahora, en el controlador basta con usar @Public() sobre cualquier método que deba ser de acceso libre. Si el nombre de la key cambia en el futuro, solo se modifica en un lugar y todos los guardianes y controladores que la referencien se actualizan automáticamente.
Al probar nuevamente [09:55], el comportamiento es idéntico: las rutas marcadas con @Public() son de acceso libre, y el resto requiere el header de autorización. La ventaja real está en la mantenibilidad y la consistencia del código a lo largo de toda la aplicación.
¿Has implementado decoradores personalizados en tus proyectos con NestJS? Comparte tu experiencia y cuéntanos qué otros patrones te han resultado útiles.
Ángel David Roque Ayala
Andres Trujillo
Gracias por las notas!
Andrés Esteban Rodríguez Jiménez
Andres Trujillo
Ty!
Lo que hicimos
Esto aplica para ambos proyectos
Para proteger todo un controlador en lugar de solo una ruta en especifico, podemos asignarle el guard al controlador.
// src\app.controller.ts ... @UseGuards(ApiKeyGuard) @Controller() ...
Podemos crear un decorador para volver publico alguna ruta protegida. Para ello, modificamos nuestro controlador
... const isPublic = this.reflector.get('isPublic', context.getHandler()); if (isPublic) { return true; } const request = context.switchToHttp().getRequest<Request>(); const authHeader = request.header('Auth'); return authHeader === '1234'; }
Creamos un decorador que nos facilite el envió de metadata a la request
// src\auth\decorators\public.decortator.ts import { SetMetadata } from '@nestjs/common' export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Ahora podemos declara como publico alguna ruta protegida
@Public() // usando el decorador @Get('nuevo') newEndpoint() { return 'yo soy nuevo'; }
Usando un decorador
Una funcionalidad muy útil a la hora de trabajar con nuestros guardianes es que en vez de nosotros tener que programar cual es la condición que debe cumplir el contexto que necesitamos, lo que podemos hacer es usar decoradores que validen esta información por nosotros:
Para nuestro ejemplo, lo que vamos a hacer es que vamos a proteger todos los Endpoints de nuestro controlador:
**src\app.controller.ts**:// importamos el "SetMetadata" import { Controller, Get, SetMetadata } from '@nestjs/common'; /* para que todos los enpoints de el controlador esten protegidos colocamos el decodador arriba del controlador */ // todos los endpoints estarán protegidos @UseGuards(ApiKeyGuard) @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() // SetMetadata nos permite enviar metadatos que podemos recibir en el contexto del guardian @SetMetadata('isPublic', true) getHello(): string { return this.appService.getHello(); } // ... }
La forma en la que vamos a utilizar este metadato que estamos enviando al contexto del guardián, en este caso va a ser para que con esa configuración protejamos todos los Endpoints del controlador, menos, el Endpoint que tenga el metadato **‘isPiblic’** en **true**.
Veamos como:
**src/auth/guards/api-key.guard.ts**:// importamos al reflector import { Reflector } from '@nestjs/core'; @Injectable() export class ApiKeyGuard implements CanActivate { // debemos inyectar el reflector en nuestro guardian constructor(private reflector: Reflector) {} canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { // con un método del reflector, nos traemos la metadata de "isPublic" // con el contexto, le indicamos que recibimos un manejador const isPublic = this.reflector.get('isPublic', context.getHandler()); // el parámetro tiene la propiedad "isPublic" como verdadera? if (isPublic) { // no hacemos validación return true; } // si en la metadata, indica que no es publico, hacemos la validación // ... } }
De esta forma, si un parámetro tiene en sus metadatos que es publico, no se realizará la validación, pero si en sus metadatos no lo tienen, re debe realizar la validación.
Creando nuestros decoradores con metadatos
El crear nuestros propios decoradores con la metadata que necesitamos es útil para que no caigamos en errores de Typos, y también tanto para agilizar la experiencia de desarrollo, como hacer el código mucho más verboso.
Para realizar esto, vamos a crear este decorador de la siguiente forma:
**src\auth\decorators\public.decortator.ts**:import { SetMetadata } from '@nestjs/common'; // instanciamos como queremos que se va a llamar la metadata // lo exportamos por si lo queremos reutilizar en otro decodador y en el guardian export const IS_PUBLIC_KEY = 'isPublic'; // creamos el decorador primero como una función flecha // va a ser igual al decodador "SetMetadata", pero con el metadato "isPublic" como "true" export const IsPublic = () => SetMetadata(IS_PUBLIC_KEY, true);
Y ahora solo tendríamos que usar este decorador de la siguiente forma:
**src\app.controller.ts**:// importamos nuestro decoador import { IsPublic } from './auth/decorators/public.decorator'; @UseGuards(ApiKeyGuard) @Controller() export class AppController { @Get() // lo usamos en el parámetro deseado @IsPublic() getHello(): string { return this.appService.getHello(); } }
Y listo, ahora tenemos un decorador un poco más flexible y personalizado.