Understanding Route Guards in Angular

Building secure and reliable web applications often requires controlling which users can access specific routes or components.
In Angular, this is achieved using Route Guards — special classes that decide whether navigation to a route should be allowed or prevented.

This comprehensive post explores every aspect of Angular Route Guards — their purpose, types, implementation, and best practices.
We will also walk through real examples of CanActivate, CanDeactivate, CanLoad, Resolve, and CanActivateChild guards.

1. Introduction to Route Guards

In Angular, route guards are used to control navigation between routes.
They help in situations such as:

  • Restricting access to authenticated users.
  • Preventing users from leaving unsaved forms.
  • Delaying route loading until data is fetched.
  • Blocking feature modules from unauthorized access.

Each guard implements a specific Angular interface, and each interface serves a distinct purpose.


2. Why Use Route Guards?

Without guards, any user could navigate to any route by simply entering a URL manually.
Guards provide a security layer that allows you to:

  1. Protect sensitive routes (like dashboard or admin panels).
  2. Prevent accidental navigation (when form data is unsaved).
  3. Load necessary data before route activation.
  4. Restrict lazy-loaded modules until conditions are met.

3. Types of Angular Route Guards

Angular provides five main types of guards:

Guard TypePurpose
CanActivateDecides if a route can be activated
CanDeactivateChecks if the user can leave a route
CanLoadDetermines if a lazy-loaded module can be loaded
ResolvePre-fetches data before activating a route
CanActivateChildProtects child routes of a given route

Let’s explore each of these in detail.


4. Setting Up Route Guards

Before creating guards, make sure your Angular app has routing configured.

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { DashboardComponent } from './dashboard.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'dashboard', component: DashboardComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Once routing is configured, you can begin creating guards.


5. CanActivate Guard

Purpose

CanActivate determines if a route can be activated.
It’s commonly used for authentication — allowing only logged-in users to access specific routes.


Example: Authentication Guard

Step 1: Create AuthService

@Injectable({ providedIn: 'root' })
export class AuthService {
  isLoggedIn = false;

  login() {
this.isLoggedIn = true;
} logout() {
this.isLoggedIn = false;
} }

Step 2: Create AuthGuard

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private auth: AuthService, private router: Router) {}

  canActivate(): boolean {
if (this.auth.isLoggedIn) {
  return true;
} else {
  this.router.navigate(['/']);
  return false;
}
} }

Step 3: Apply Guard to Routes

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }
];

Now, users cannot access /dashboard unless logged in.


6. CanDeactivate Guard

Purpose

CanDeactivate checks if a user can leave a route.
It is often used to prevent users from losing unsaved data when navigating away.


Example: Unsaved Form Guard

Step 1: Create a Component with Form

@Component({
  selector: 'app-edit-profile',
  template: `
<form>
  <input type="text" [(ngModel)]="name" />
  <button>Save</button>
</form>
` }) export class EditProfileComponent { name = ''; saved = false; canDeactivate(): boolean {
return this.saved || confirm('You have unsaved changes. Leave anyway?');
} }

Step 2: Create CanDeactivate Guard

import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';

export interface CanComponentDeactivate {
  canDeactivate: () => boolean;
}

@Injectable({ providedIn: 'root' })
export class DeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate): boolean {
return component.canDeactivate ? component.canDeactivate() : true;
} }

Step 3: Apply to Route

const routes: Routes = [
  { path: 'edit', component: EditProfileComponent, canDeactivate: [DeactivateGuard] }
];

Now, if the user tries to navigate away without saving, a confirmation appears.


7. CanLoad Guard

Purpose

CanLoad prevents a lazy-loaded module from being loaded if the user doesn’t meet conditions, such as authentication.

This guard runs before the module is even downloaded, improving performance and security.


Example: Restrict Lazy Loading

Step 1: Define a Lazy Module

const routes: Routes = [
  { path: '', component: DashboardComponent }
];
@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class DashboardRoutingModule {}

Step 2: Protect It in App Routing

const routes: Routes = [
  {
path: 'dashboard',
loadChildren: () =&gt; import('./dashboard/dashboard.module').then(m =&gt; m.DashboardModule),
canLoad: &#91;AuthGuard]
} ];

Step 3: Update AuthGuard

import { CanLoad, Route, UrlSegment } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanLoad {
  constructor(private auth: AuthService, private router: Router) {}

  canLoad(route: Route, segments: UrlSegment[]): boolean {
if (this.auth.isLoggedIn) {
  return true;
} else {
  this.router.navigate(&#91;'/']);
  return false;
}
} }

This ensures the dashboard module is loaded only for authenticated users.


8. Resolve Guard

Purpose

Resolve guard is used to pre-fetch data before a route is activated.
This ensures that required data is available as soon as the component loads.


Example: Data Resolver

Step 1: Create a Resolver

import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable, of } from 'rxjs';
import { UserService } from './user.service';

@Injectable({ providedIn: 'root' })
export class UserResolver implements Resolve<any> {
  constructor(private userService: UserService) {}

  resolve(): Observable<any> {
return this.userService.getUser();
} }

Step 2: Apply to Route

const routes: Routes = [
  {
path: 'profile',
component: ProfileComponent,
resolve: { user: UserResolver }
} ];

Step 3: Access Resolved Data in Component

@Component({
  selector: 'app-profile',
  template: &lt;h2&gt;{{ user?.name }}&lt;/h2&gt;
})
export class ProfileComponent {
  user: any;
  constructor(private route: ActivatedRoute) {
this.user = this.route.snapshot.data&#91;'user'];
} }

This ensures the component renders only after the user data has been fetched.


9. CanActivateChild Guard

Purpose

CanActivateChild is used to protect child routes of a parent route.
It ensures that child routes are accessible only if certain conditions are met.


Example: Protecting Child Routes

Step 1: Create AdminGuard

@Injectable({ providedIn: 'root' })
export class AdminGuard implements CanActivateChild {
  constructor(private auth: AuthService, private router: Router) {}

  canActivateChild(): boolean {
if (this.auth.isLoggedIn) {
  return true;
} else {
  this.router.navigate(&#91;'/']);
  return false;
}
} }

Step 2: Apply to Route

const routes: Routes = [
  {
path: 'admin',
component: AdminComponent,
canActivateChild: &#91;AdminGuard],
children: &#91;
  { path: 'users', component: UsersComponent },
  { path: 'settings', component: SettingsComponent }
]
} ];

Now, all child routes of /admin are protected.


10. Combining Multiple Guards

You can use multiple guards on the same route.

const routes: Routes = [
  {
path: 'dashboard',
component: DashboardComponent,
canActivate: &#91;AuthGuard],
canDeactivate: &#91;DeactivateGuard]
} ];

Here, both authentication and unsaved-changes checks will run before navigation.


11. Returning Observables or Promises

Guards can return:

  • boolean
  • Observable<boolean>
  • Promise<boolean>

Example returning Observable:

canActivate(): Observable<boolean> {
  return this.authService.checkLoginStatus();
}

This allows asynchronous checks like API validation or token verification.


12. Guard Execution Order

Angular executes guards in this order:

  1. CanDeactivate
  2. CanLoad
  3. CanActivateChild
  4. CanActivate
  5. Resolve

If any guard returns false, navigation is canceled.


13. Redirecting Within Guards

You can programmatically redirect inside a guard using the Angular Router.

canActivate(): boolean {
  if (!this.auth.isLoggedIn) {
this.router.navigate(&#91;'/login']);
return false;
} return true; }

This ensures users are redirected instead of simply being blocked.


14. Handling Async Guards with RxJS

Example using RxJS operators:

canActivate(): Observable<boolean> {
  return this.authService.user$.pipe(
map(user =&gt; !!user),
tap(isLoggedIn =&gt; {
  if (!isLoggedIn) this.router.navigate(&#91;'/login']);
})
); }

This approach integrates seamlessly with reactive authentication systems.


15. Common Use Cases

Use CaseGuard Type
Restrict route to authenticated usersCanActivate
Confirm before leaving formCanDeactivate
Block unauthorized module loadingCanLoad
Fetch data before route loadsResolve
Protect child routes under a parentCanActivateChild

16. Testing Route Guards

Guards can be unit tested like any Angular service.

Example test:

it('should block navigation when not logged in', () => {
  const authService = { isLoggedIn: false } as AuthService;
  const guard = new AuthGuard(authService as any, router as any);
  expect(guard.canActivate()).toBeFalse();
});

Testing guards ensures predictable navigation behavior across environments.


17. Best Practices for Route Guards

  1. Keep guards lightweight.
    Do not overload them with complex logic.
  2. Avoid API calls inside guards when possible.
    Use Resolve for data loading.
  3. Reuse logic through services.
    Authentication or permission checks should live in separate services.
  4. Return Observables for asynchronous checks.
  5. Use CanLoad for lazy-loaded modules.
  6. Keep guards pure and side-effect-free except for navigation redirection.
  7. Combine guards logically.
    Example: AuthGuard + RoleGuard for admin routes.
  8. Handle navigation errors gracefully.
  9. Always test your guards.

18. Example: Full Implementation

Here’s an example combining multiple guards in a real-world scenario.

const routes: Routes = [
  {
path: 'admin',
component: AdminComponent,
canActivate: &#91;AuthGuard],
canActivateChild: &#91;RoleGuard],
canLoad: &#91;AuthGuard],
children: &#91;
  {
    path: 'users',
    component: UsersComponent,
    resolve: { users: UsersResolver },
    canDeactivate: &#91;DeactivateGuard]
  }
]
} ];

This configuration ensures:

  • Only authenticated users can load and access admin routes.
  • Child routes require special roles.
  • User data is preloaded.
  • Leaving without saving changes prompts a confirmation.

19. Handling Roles and Permissions

Guards can also handle role-based access control.

@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivate {
  constructor(private auth: AuthService, private router: Router) {}

  canActivate(): boolean {
const user = this.auth.getUser();
if (user.role === 'admin') {
  return true;
} else {
  this.router.navigate(&#91;'/unauthorized']);
  return false;
}
} }

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *