Guards funcionales con inject()
La evolución del sistema de routing en Angular ha marcado un hito importante con la introducción de los guards funcionales desde Angular 15. Esta transformación representa un cambio paradigmático que abandona las clases tradicionales en favor de funciones más simples y composables.
Evolución desde guards basados en clases
Los guards tradicionales requerían la implementación de interfaces específicas como CanActivate
, CanMatch
o CanDeactivate
en clases dedicadas. Este enfoque, aunque funcional, introducía boilerplate innecesario y complicaba la inyección de dependencias.
// Approach tradicional (deprecado)
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean {
if (this.authService.isAuthenticated()) {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
Funciones como guards modernos
Los guards funcionales eliminan la necesidad de clases, permitiendo definir la lógica de protección directamente como funciones. Esta aproximación resulta más declarativa y reduce significativamente la cantidad de código necesario.
// Approach funcional moderno
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
router.navigate(['/login']);
return false;
};
La función inject() como elemento central
La función inject() constituye el mecanismo fundamental que permite acceder al sistema de inyección de dependencias dentro de guards funcionales. Esta función solo puede ejecutarse durante el contexto de inyección, típicamente durante la construcción de componentes, servicios o guards.
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
export const roleGuard: CanActivateFn = (route, state) => {
const userService = inject(UserService);
const router = inject(Router);
const requiredRole = route.data?.['role'];
const userRole = userService.getCurrentUserRole();
if (userRole === requiredRole) {
return true;
}
return router.parseUrl('/unauthorized');
};
Ventajas del approach funcional
La adopción de guards funcionales aporta múltiples beneficios que mejoran tanto la experiencia de desarrollo como el mantenimiento del código:
- Reducción de boilerplate: Eliminan la necesidad de clases, constructores e implementación de interfaces
- Mejor tree-shaking: Las funciones no utilizadas se eliminan automáticamente del bundle final
- Composabilidad mejorada: Facilitan la reutilización y combinación de lógica de guards
- Testing simplificado: Las funciones puras son más fáciles de testear que las clases con dependencias
- Mejor rendimiento: Menor overhead al evitar instanciación de clases
Integración con el sistema de rutas
Los guards funcionales se integran de forma transparente en la configuración de rutas, manteniendo la misma sintaxis que los guards tradicionales pero con una implementación más limpia.
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard, roleGuard]
},
{
path: 'profile',
component: ProfileComponent,
canActivate: [authGuard]
}
];
Guards condicionales y composición
Los guards funcionales permiten implementar lógica condicional compleja de forma más elegante, facilitando la composición de múltiples condiciones.
export const conditionalGuard: CanActivateFn = (route, state) => {
const featureService = inject(FeatureToggleService);
const authService = inject(AuthService);
// Guard condicional basado en feature flags
if (!featureService.isEnabled('new-feature')) {
return inject(Router).parseUrl('/feature-disabled');
}
return authService.isAuthenticated();
};
Este enfoque funcional establece las bases sólidas para implementar sistemas de protección de rutas más mantenibles y eficientes, preparando el terreno para explorar los tipos específicos de guards y sus casos de uso particulares.
CanActivate, CanMatch y CanDeactivate modernos
Los guards funcionales en Angular se materializan a través de tres tipos principales, cada uno con un propósito específico en el ciclo de navegación. Estos guards modernos utilizan tipos de función específicos que reemplazan las interfaces tradicionales.
CanActivate funcional
El guard CanActivate
controla si una ruta puede ser activada cuando el usuario intenta navegar hacia ella. Su versión funcional utiliza el tipo CanActivateFn
y recibe los parámetros route
y state
.
import { CanActivateFn, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
export const userAccessGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) => {
const authService = inject(AuthService);
const userService = inject(UserService);
if (!authService.isAuthenticated()) {
return inject(Router).parseUrl('/login');
}
const userId = route.params['id'];
const currentUser = userService.getCurrentUser();
// Solo permitir acceso si es el propio usuario o un admin
return currentUser.id === userId || currentUser.isAdmin;
};
CanMatch funcional
El guard CanMatch
determina si una configuración de ruta coincide con la URL actual. Es especialmente útil para lazy loading condicional y cuando múltiples rutas podrían coincidir con la misma URL.
export const featureAccessGuard: CanMatchFn = (route, segments) => {
const featureService = inject(FeatureToggleService);
const authService = inject(AuthService);
const featureName = route.data?.['feature'];
// La ruta solo coincide si el usuario está autenticado
// y tiene acceso a la feature específica
return authService.isAuthenticated() &&
featureService.hasAccess(featureName);
};
Configuración en rutas con CanMatch:
const routes: Routes = [
{
path: 'premium-dashboard',
loadComponent: () => import('./premium-dashboard.component'),
canMatch: [featureAccessGuard],
data: { feature: 'premium-features' }
},
{
path: 'premium-dashboard',
loadComponent: () => import('./basic-dashboard.component')
// Fallback si el guard anterior no permite la coincidencia
}
];
CanDeactivate funcional
El guard CanDeactivate
controla si el usuario puede abandonar la ruta actual. Es fundamental para prevenir la pérdida de datos no guardados o confirmar acciones destructivas.
export interface CanComponentDeactivate {
canDeactivate(): boolean | Promise<boolean> | Observable<boolean>;
}
export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (
component: CanComponentDeactivate
) => {
if (component.canDeactivate && !component.canDeactivate()) {
return confirm('¿Estás seguro de que quieres salir? Los cambios no guardados se perderán.');
}
return true;
};
Implementación en componente:
@Component({
selector: 'app-form-editor',
standalone: true,
template: `
<form [formGroup]="editorForm">
<input formControlName="title" placeholder="Título">
<textarea formControlName="content" placeholder="Contenido"></textarea>
<button (click)="save()" [disabled]="!hasUnsavedChanges">Guardar</button>
</form>
`
})
export class FormEditorComponent implements CanComponentDeactivate {
editorForm = inject(FormBuilder).group({
title: [''],
content: ['']
});
private initialValue = this.editorForm.getRawValue();
get hasUnsavedChanges(): boolean {
const currentValue = this.editorForm.getRawValue();
return JSON.stringify(currentValue) !== JSON.stringify(this.initialValue);
}
canDeactivate(): boolean {
return !this.hasUnsavedChanges;
}
save(): void {
// Lógica de guardado
this.initialValue = this.editorForm.getRawValue();
}
}
Guards asíncronos y observables
Los guards funcionales soportan operaciones asíncronas mediante Promises y Observables, permitiendo validaciones complejas como verificaciones de permisos en el servidor.
export const serverValidationGuard: CanActivateFn = (route, state) => {
const permissionService = inject(PermissionService);
const loadingService = inject(LoadingService);
loadingService.show();
return permissionService.checkPermission(route.data?.['permission']).pipe(
map(hasPermission => {
if (!hasPermission) {
return inject(Router).parseUrl('/access-denied');
}
return true;
}),
catchError(() => {
return of(inject(Router).parseUrl('/error'));
}),
finalize(() => loadingService.hide())
);
};
Manejo de redirecciones con UrlTree
Los guards modernos pueden devolver UrlTree para realizar redirecciones más precisas que las navegaciones tradicionales, manteniendo el contexto de la navegación.
export const subscriptionGuard: CanActivateFn = (route, state) => {
const subscriptionService = inject(SubscriptionService);
const router = inject(Router);
const user = inject(UserService).getCurrentUser();
if (!user.hasActiveSubscription) {
// Redirección con query params para preservar el destino original
return router.createUrlTree(['/subscription-required'], {
queryParams: { returnUrl: state.url }
});
}
return true;
};
Testing de guards funcionales
Los guards funcionales simplifican considerablemente las pruebas unitarias al ser funciones puras que pueden testearse de forma aislada.
describe('authGuard', () => {
let mockAuthService: jasmine.SpyObj<AuthService>;
let mockRouter: jasmine.SpyObj<Router>;
beforeEach(() => {
mockAuthService = jasmine.createSpyObj('AuthService', ['isAuthenticated']);
mockRouter = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useValue: mockAuthService },
{ provide: Router, useValue: mockRouter }
]
});
});
it('should allow access for authenticated users', () => {
mockAuthService.isAuthenticated.and.returnValue(true);
TestBed.runInInjectionContext(() => {
const result = authGuard(
{} as ActivatedRouteSnapshot,
{} as RouterStateSnapshot
);
expect(result).toBe(true);
});
});
});
La implementación de estos tres tipos de guards funcionales proporciona un control granular sobre la navegación, manteniendo el código más limpio y testeable que sus equivalentes basados en clases.

Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Angular
Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender la evolución de guards basados en clases a guards funcionales en Angular.
- Aprender a utilizar la función inject() para acceder a dependencias dentro de guards funcionales.
- Diferenciar y aplicar los tipos de guards funcionales: CanActivate, CanMatch y CanDeactivate.
- Implementar guards funcionales con lógica condicional, asíncrona y manejo de redirecciones mediante UrlTree.
- Realizar pruebas unitarias efectivas sobre guards funcionales para garantizar su correcto funcionamiento.