Angular Route Guards
Introduction
Route Guards are a powerful feature in Angular's routing system that allow you to control access to specific routes in your application. They act as gatekeepers that can prevent unauthorized users from accessing certain parts of your application, redirect users to login pages, prevent users from leaving a page with unsaved changes, and much more.
In this tutorial, we'll explore different types of Angular Route Guards, understand how they work, and implement them in real-world scenarios to secure our application routes.
What are Route Guards?
Route Guards are interfaces provided by the Angular Router that allow you to control the accessibility of routes based on conditions you define. Before a route is activated or deactivated, Angular checks if all the guards return true
. If any guard returns false
, the navigation is cancelled.
Angular provides several built-in guard interfaces:
- CanActivate: Controls if a route can be activated
- CanActivateChild: Controls if children of a route can be activated
- CanDeactivate: Controls if a user can leave a route
- Resolve: Performs route data retrieval before route activation
- CanLoad: Controls if a module can be loaded lazily
Creating Your First Route Guard
Let's start by creating a simple CanActivate
guard that checks if a user is logged in before allowing access to a route.
First, we'll generate a new guard using the Angular CLI:
ng generate guard auth/auth
When prompted, select CanActivate
as the interface you want to implement. The CLI will create a new guard file for you.
Now, let's implement our auth guard:
// auth.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
if (this.authService.isLoggedIn()) {
return true;
} else {
// Redirect to the login page
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
}
}
In this example, we're using a hypothetical AuthService
that has an isLoggedIn()
method to check if the user is authenticated. If the user is not logged in, we redirect them to the login page and pass the current URL as a query parameter, so we can redirect them back after successful login.
Using the Route Guard in Routes
Once we have created our guard, we need to apply it to our routes. Let's see how to do that:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { LoginComponent } from './login/login.component';
import { AuthGuard } from './auth/auth.guard';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{
path: 'profile',
component: ProfileComponent,
canActivate: [AuthGuard] // Apply our guard here
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canActivate: [AuthGuard] // Protect the entire admin module
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Now, the /profile
route and the /admin
module are protected by our AuthGuard
. Users will only be able to access these routes if they are logged in.
Implementing Different Types of Guards
CanActivateChild Guard
The CanActivateChild
guard is similar to the CanActivate
guard, but it controls access to child routes. Let's extend our AuthGuard
to implement this interface:
// auth.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
CanActivateChild,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
return this.checkLogin(state.url);
}
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
return this.canActivate(childRoute, state);
}
private checkLogin(url: string): boolean {
if (this.authService.isLoggedIn()) {
return true;
}
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page
this.router.navigate(['/login']);
return false;
}
}
Now we can use this guard to protect child routes:
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivateChild: [AuthGuard],
children: [
{ path: 'dashboard', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent },
{ path: 'settings', component: AdminSettingsComponent }
]
}
];
CanDeactivate Guard
The CanDeactivate
guard is useful when you want to prevent users from accidentally leaving a page with unsaved changes. Let's create a new guard for this purpose:
// can-deactivate.guard.ts
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;
}
}
Now, let's implement this interface in a component:
// edit-form.component.ts
import { Component } from '@angular/core';
import { CanComponentDeactivate } from './can-deactivate.guard';
@Component({
selector: 'app-edit-form',
template: `
<form [formGroup]="form">
<input formControlName="name" placeholder="Name">
<button (click)="save()">Save</button>
</form>
`
})
export class EditFormComponent implements CanComponentDeactivate {
form: FormGroup;
saved = false;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
name: ['']
});
// Track form changes
this.form.valueChanges.subscribe(() => {
this.saved = false;
});
}
save() {
// Save the form data
this.saved = true;
}
canDeactivate(): boolean {
if (this.form.dirty && !this.saved) {
return confirm('You have unsaved changes! Do you really want to leave?');
}
return true;
}
}
And finally, add the guard to our route:
// app-routing.module.ts
const routes: Routes = [
{
path: 'edit',
component: EditFormComponent,
canDeactivate: [CanDeactivateGuard]
}
];
Resolve Guard
The Resolve
guard is used to fetch data before a route is activated. This is useful when you want to ensure that certain data is available before the component is instantiated.
First, let's create a resolver:
// user-resolver.service.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { UserService } from './user.service';
import { User } from './user.model';
@Injectable({
providedIn: 'root'
})
export class UserResolver implements Resolve<User> {
constructor(private userService: UserService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<User> {
const userId = route.paramMap.get('id');
return this.userService.getUser(userId).pipe(
catchError(error => {
console.error('Error retrieving user:', error);
return of(null); // Return a default value or redirect
})
);
}
}
Now, let's use this resolver in our routes:
// app-routing.module.ts
const routes: Routes = [
{
path: 'user/:id',
component: UserDetailComponent,
resolve: {
user: UserResolver
}
}
];
And in our component, we can access the resolved data:
// user-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { User } from './user.model';
@Component({
selector: 'app-user-detail',
template: `
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserDetailComponent implements OnInit {
user: User;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// The resolved user is available in the route's data
this.user = this.route.snapshot.data['user'];
}
}
CanLoad Guard
The CanLoad
guard controls whether a module can be lazy loaded. This is useful for preventing unauthorized users from downloading modules they don't have permission to access.
// auth.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
CanLoad,
Route,
UrlSegment,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanLoad {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
return this.checkLogin(state.url);
}
canLoad(
route: Route,
segments: UrlSegment[]
): boolean {
const url = `/${segments.map(s => s.path).join('/')}`;
return this.checkLogin(url);
}
private checkLogin(url: string): boolean {
if (this.authService.isLoggedIn()) {
return true;
}
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page
this.router.navigate(['/login']);
return false;
}
}
Now, let's use this guard to protect a lazy-loaded module:
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AuthGuard]
}
];
Real-World Application: Role-Based Route Protection
Let's create a more sophisticated route guard that checks not just if a user is logged in, but also if they have the required role to access a route:
// role.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
// Get the required roles from the route data
const requiredRoles = route.data['roles'] as Array<string>;
// Check if the user has the required roles
if (this.authService.isLoggedIn() &&
this.checkRoles(requiredRoles)) {
return true;
}
// User doesn't have required roles, redirect to access denied page
if (this.authService.isLoggedIn()) {
this.router.navigate(['/access-denied']);
} else {
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
}
return false;
}
private checkRoles(requiredRoles: string[]): boolean {
// If no roles are required, allow access
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
// Check if the user has any of the required roles
const userRoles = this.authService.getCurrentUserRoles();
return requiredRoles.some(role => userRoles.includes(role));
}
}
Now, let's use this guard with route data:
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [RoleGuard],
data: { roles: ['ADMIN'] }
},
{
path: 'manager',
component: ManagerComponent,
canActivate: [RoleGuard],
data: { roles: ['MANAGER', 'ADMIN'] }
},
{
path: 'user-dashboard',
component: UserDashboardComponent,
canActivate: [RoleGuard],
data: { roles: ['USER', 'MANAGER', 'ADMIN'] }
},
{
path: 'access-denied',
component: AccessDeniedComponent
}
];
In this example, the admin
route requires the 'ADMIN' role, the manager
route requires either the 'MANAGER' or 'ADMIN' role, and the user-dashboard
route is accessible to users with any of the three roles.
Summary
Angular Route Guards are powerful tools for controlling navigation in your application. They allow you to:
- Protect routes from unauthorized access with
CanActivate
andCanActivateChild
- Prevent users from leaving a page with unsaved changes using
CanDeactivate
- Preload data before navigating to a route with
Resolve
- Control lazy loading of modules with
CanLoad
By using Route Guards effectively, you can create a more secure and user-friendly application that directs users appropriately based on their authentication status, permissions, and actions.
Additional Resources
Exercises
- Create a new Angular application with a login page and at least three protected routes.
- Implement an
AuthGuard
that checks if a user is logged in. - Implement a
RoleGuard
that restricts access based on user roles. - Create a form component with a
CanDeactivate
guard that warns users about unsaved changes. - Implement a
Resolve
guard to prefetch data for a user profile page.
By completing these exercises, you will gain a solid understanding of how to implement and use Angular Route Guards in real-world scenarios.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)