DEV Community

Cover image for Angular Guards and Route Protection: Complete Guide | CanActivate, CanDeactivate & Resolve
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Angular Guards and Route Protection: Complete Guide | CanActivate, CanDeactivate & Resolve

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)
  }
];
Enter fullscreen mode Exit fullscreen mode

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' }
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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'
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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'];
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
];
Enter fullscreen mode Exit fullscreen mode

Guard Execution Order

Guards execute in the order they're defined:

  1. CanLoad - Prevents module loading
  2. CanActivate - Checks route access (all must return true)
  3. Resolve - Prefetches data
  4. 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' }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Use CanActivate for authentication - Check if user is logged in
  2. Use CanActivate for authorization - Check user roles and permissions
  3. Use CanDeactivate for unsaved changes - Prevent data loss
  4. Use CanLoad for lazy modules - Prevent unauthorized module loading
  5. Use Resolve for data prefetching - Improve user experience
  6. Store return URLs - Redirect users after authentication
  7. Handle guard failures gracefully - Show user-friendly error messages
  8. Combine multiple guards - Use guard chains for complex protection
  9. Use route data - Pass configuration to guards
  10. Test guards independently - Unit test guard logic separately
  11. Document guard behavior - Document requirements and behavior
  12. 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
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

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)