Building a secure Angular application means more than just hiding UI elements. I've seen applications where routes were "protected" by hiding navigation links, but users could still access protected routes by typing URLs directly. That's not securityβthat's security theater. Route Guards are Angular's way of actually protecting routes at the framework level.
Route Guards are interfaces that control navigation to and from routes. They can check authentication status, verify user permissions, prevent navigation when there are unsaved changes, and even prefetch data before a route activates. They're essential for building secure, user-friendly Angular applications.
π Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
What are Angular Route Guards?
Angular Route Guards provide:
- CanActivate - Control access to routes (authentication/authorization)
- CanDeactivate - Prevent navigation away from routes (unsaved changes)
- CanLoad - Prevent lazy-loaded modules from loading
- Resolve - Prefetch data before route activation
- Security - Framework-level route protection
- User Experience - Prevent data loss and improve performance
CanActivate Guard
Protect routes with authentication and authorization:
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../auth/auth.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.isAuthenticated.pipe(
map(isAuthenticated => {
if (!isAuthenticated) {
localStorage.setItem('returnUrl', state.url);
this.authService.Logout();
this.router.navigate(['/login']);
return false;
}
return true;
})
);
}
}
// Use in routes
const routes: Routes = [
{
path: 'business',
canActivate: [AuthGuard],
loadChildren: () => import('./business/business.module').then(m => m.BusinessModule)
}
];
Role-Based CanActivate Guard
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
const requiredRole = route.data['role'];
const userRole = this.authService.getUserRole();
if (userRole !== requiredRole) {
this.router.navigate(['/unauthorized']);
return false;
}
return true;
}
}
// Use in routes
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, RoleGuard],
data: { role: 'admin' }
}
CanDeactivate Guard
Prevent navigation with unsaved changes:
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
@Injectable({
providedIn: 'root'
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(
component: CanComponentDeactivate
): Observable<boolean> | Promise<boolean> | boolean {
return component.canDeactivate ? component.canDeactivate() : true;
}
}
// Component implementation
export class BusinessFormComponent implements CanComponentDeactivate {
form: FormGroup;
isDirty: boolean = false;
canDeactivate(): boolean {
if (this.form.dirty && !this.isDirty) {
return confirm('You have unsaved changes. Do you want to leave?');
}
return true;
}
}
// Use in routes
{
path: 'business/:id/edit',
component: BusinessFormComponent,
canDeactivate: [CanDeactivateGuard]
}
Advanced CanDeactivate with Observable
export class BusinessFormComponent implements CanComponentDeactivate {
form: FormGroup;
private unsavedChanges$ = new BehaviorSubject<boolean>(false);
canDeactivate(): Observable<boolean> {
if (this.form.dirty) {
return this.showUnsavedChangesDialog();
}
return of(true);
}
private showUnsavedChangesDialog(): Observable<boolean> {
// Show custom dialog and return Observable
return this.dialogService.confirm({
title: 'Unsaved Changes',
message: 'You have unsaved changes. Do you want to leave?',
confirmText: 'Leave',
cancelText: 'Stay'
});
}
}
CanLoad Guard
Prevent lazy module loading for unauthorized users:
import { Injectable } from '@angular/core';
import { CanLoad, Route } from '@angular/router';
import { AuthService } from '../auth/auth.service';
@Injectable({
providedIn: 'root'
})
export class CanLoadGuard implements CanLoad {
constructor(private authService: AuthService) {}
canLoad(route: Route): boolean {
const requiredPermission = route.data?.['permission'];
if (!this.authService.isAuthenticated) {
return false;
}
if (requiredPermission && !this.authService.hasPermission(requiredPermission)) {
return false;
}
return true;
}
}
// Use in routes
{
path: 'admin',
canLoad: [CanLoadGuard],
data: { permission: 'admin' },
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
Key Difference: CanLoad prevents the module from being loaded at all, while CanActivate allows the module to load but prevents navigation. Use CanLoad for better performance and security.
Resolve Guard
Prefetch data before route activation:
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { BusinessService } from '../services/business.service';
@Injectable({
providedIn: 'root'
})
export class BusinessResolver implements Resolve<any> {
constructor(private businessService: BusinessService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> | Promise<any> | any {
const businessId = +route.params['id'];
return this.businessService.GetBusiness(businessId);
}
}
// Use in routes
{
path: 'business/:id',
component: BusinessDetailsComponent,
resolve: { business: BusinessResolver }
}
// Access in component
export class BusinessDetailsComponent implements OnInit {
business: Business;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
// Data is already loaded by resolver
this.business = this.route.snapshot.data['business'];
// Or subscribe to data changes
this.route.data.subscribe(data => {
this.business = data['business'];
});
}
}
Typed Resolver
@Injectable({
providedIn: 'root'
})
export class BusinessResolver implements Resolve<Business> {
constructor(private businessService: BusinessService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<Business> {
const businessId = +route.params['id'];
return this.businessService.GetBusiness(businessId);
}
}
Multiple Guards
Combine multiple guards for complex protection:
const routes: Routes = [
{
path: 'business',
canActivate: [AuthGuard, RoleGuard, PermissionGuard],
canLoad: [CanLoadGuard],
data: {
layout: 'show',
userType: loginUserType.Admin,
permission: 'business:read'
},
loadChildren: () => import('./business/business.module').then(m => m.BusinessModule)
}
];
Guard Execution Order
Guards execute in the order they're defined:
- CanLoad - Prevents module loading
- CanActivate - Checks route access (all must return true)
- Resolve - Prefetches data
- CanDeactivate - Checks if user can leave current route
Advanced Patterns
Guard with Route Data
@Injectable({
providedIn: 'root'
})
export class PermissionGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
const requiredPermission = route.data['permission'];
const userPermissions = this.authService.getUserPermissions();
if (!userPermissions.includes(requiredPermission)) {
this.router.navigate(['/forbidden']);
return false;
}
return true;
}
}
// Use with route data
{
path: 'business',
component: BusinessComponent,
canActivate: [AuthGuard, PermissionGuard],
data: { permission: 'business:read' }
}
Async Guard with Token Refresh
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authService.isAuthenticated.pipe(
take(1),
switchMap(isAuthenticated => {
if (isAuthenticated) {
// Check token expiration
if (this.authService.isTokenExpired()) {
return this.authService.refreshToken().pipe(
map(() => true),
catchError(() => {
this.router.navigate(['/login']);
return of(false);
})
);
}
return of(true);
} else {
localStorage.setItem('returnUrl', state.url);
this.router.navigate(['/login']);
return of(false);
}
})
);
}
}
Guard with Return URL
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
if (!this.authService.isAuthenticated) {
// Store the attempted URL for redirecting after login
localStorage.setItem('returnUrl', state.url);
this.router.navigate(['/login']);
return false;
}
return true;
}
}
// In login component
onLoginSuccess(): void {
const returnUrl = localStorage.getItem('returnUrl') || '/dashboard';
localStorage.removeItem('returnUrl');
this.router.navigate([returnUrl]);
}
Best Practices
- Use CanActivate for authentication - Check if user is logged in
- Use CanActivate for authorization - Check user roles and permissions
- Use CanDeactivate for unsaved changes - Prevent data loss
- Use CanLoad for lazy modules - Prevent unauthorized module loading
- Use Resolve for data prefetching - Improve user experience
- Store return URLs - Redirect users after authentication
- Handle guard failures gracefully - Show user-friendly error messages
- Combine multiple guards - Use guard chains for complex protection
- Use route data - Pass configuration to guards
- Test guards independently - Unit test guard logic separately
- Document guard behavior - Document requirements and behavior
- Handle async operations - Use Observables/Promises for async checks
Common Patterns
Guard Factory
export function canActivateAdmin(authService: AuthService, router: Router) {
return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean => {
if (authService.isAdmin()) {
return true;
}
router.navigate(['/unauthorized']);
return false;
};
}
// Use in routes
{
path: 'admin',
canActivate: [canActivateAdmin],
component: AdminComponent
}
Conditional Guard Application
@Injectable({
providedIn: 'root'
})
export class ConditionalGuard implements CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
const skipGuard = route.data['skipGuard'];
if (skipGuard) {
return true;
}
// Apply guard logic
return this.checkAccess();
}
}
Resources and Further Reading
- π Full Angular Guards Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- Angular Routing Guide - Routing patterns and navigation
- Angular Services Guide - Dependency injection for guard services
- Angular Reactive Forms Guide - Form handling with guards
- Angular Route Guards Documentation - Official Angular docs
- Angular CanActivate API - CanActivate reference
- Angular CanDeactivate API - CanDeactivate reference
Conclusion
Angular Route Guards provide essential security and user experience features. By implementing appropriate guards, you can protect routes, prevent unauthorized access, handle unsaved changes, and improve application performance with data prefetching.
Key Takeaways:
- CanActivate - Control route access (authentication/authorization)
- CanDeactivate - Prevent navigation with unsaved changes
- CanLoad - Prevent unauthorized module loading
- Resolve - Prefetch data before route activation
- Multiple guards - Combine guards for complex protection
- Route data - Pass configuration to guards
- Return URLs - Redirect users after authentication
- Async guards - Handle async operations with Observables/Promises
Whether you're building a simple authentication system or a complex role-based access control system, Angular Route Guards provide the foundation you need. They handle all the route protection logic while giving you complete control over navigation and security.
What's your experience with Angular Route Guards? Share your tips and tricks in the comments below! π
π‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.
Top comments (0)