Route Guards in Angular
Route guards are functions (or classes) that Angular's router calls before activating, deactivating, or loading a route. They let you:
- Protect pages from unauthenticated users
- Prevent leaving a form with unsaved changes
- Control whether a lazy-loaded module is downloaded at all
- Pre-fetch data before a component renders (resolvers)
Angular 14.2+ prefers functional guards — plain functions that return boolean, UrlTree, or an Observable/Promise of either. Class-based guards still work but the functional style is simpler and more testable.
Guard Types Overview
Guard type | Route property | Purpose |
|---|---|---|
canActivate | canActivate | Prevent entering a route |
canActivateChild | canActivateChild | Prevent entering any child route |
canDeactivate | canDeactivate | Prevent leaving a route (unsaved changes) |
canMatch | canMatch | Decide whether a route should even be considered |
canLoad (deprecated) | canLoad | Prevent loading a lazy module (use canMatch) |
resolve | resolve | Pre-fetch data before the route activates |
canActivate: Protecting a Route
The most common guard. Returns true to allow navigation, false to block it, or a UrlTree to redirect.
// auth.guard.ts — functional guard
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true; // allow navigation
}
// Redirect to login, preserving the intended URL
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};// app.routes.ts — apply the guard
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard],
},
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard, adminGuard], // multiple guards — ALL must pass
},
];Role-Based Guard
// role.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, ActivatedRouteSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
const auth = inject(AuthService);
const router = inject(Router);
const requiredRole: string = route.data['role'];
const userRole = auth.currentUser()?.role;
if (userRole === requiredRole) return true;
return router.createUrlTree(['/forbidden']);
};
// app.routes.ts
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard, roleGuard],
data: { role: 'ADMIN' },
}canActivateChild: Protecting All Child Routes
Apply a guard to all children of a parent route in one place rather than repeating it on every child.
// auth.guard.ts — the same function works as canActivateChild too
import { CanActivateChildFn } from '@angular/router';
export const authGuard: CanActivateChildFn = (childRoute, state) => {
// same logic as canActivate
const auth = inject(AuthService);
return auth.isLoggedIn() || inject(Router).createUrlTree(['/login']);
};
// app.routes.ts
{
path: 'admin',
component: AdminShellComponent,
canActivateChild: [authGuard], // applies to ALL children
children: [
{ path: '', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent },
{ path: 'config', component: AdminConfigComponent },
],
}canDeactivate: Preventing Accidental Navigation Away
Warn users when they try to leave a form with unsaved changes.
// unsaved-changes.guard.ts
import { CanDeactivateFn } from '@angular/router';
// The guard can call a method on the component
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (component.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Leave anyway?');
}
return true;
};// edit-profile.component.ts
import { Component } from '@angular/core';
import { HasUnsavedChanges } from './unsaved-changes.guard';
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
@Component({ standalone: true, imports: [ReactiveFormsModule], template: `...` })
export class EditProfileComponent implements HasUnsavedChanges {
private fb = inject(FormBuilder);
form = this.fb.group({ name: [''], bio: [''] });
hasUnsavedChanges(): boolean {
return this.form.dirty;
}
}
// app.routes.ts
{
path: 'profile/edit',
component: EditProfileComponent,
canDeactivate: [unsavedChangesGuard],
}confirm() dialogs are blocked in some contexts. For production apps, implement a custom modal-based confirmation instead of relying on the browser's native dialog.canMatch: Preventing Lazy Chunk Download
canMatch runs before the route is even considered — meaning Angular won't download the lazy chunk for an unauthorised user. This is more secure than canActivate, which runs after the chunk is already downloaded.
// admin-match.guard.ts
import { inject } from '@angular/core';
import { CanMatchFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const adminMatchGuard: CanMatchFn = () => {
const auth = inject(AuthService);
if (auth.hasRole('ADMIN')) return true;
inject(Router).navigate(['/forbidden']);
return false;
};
// app.routes.ts
{
path: 'admin',
canMatch: [adminMatchGuard], // chunk NOT downloaded for non-admins
loadChildren: () =>
import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
}canMatch over the deprecated canLoad for all new code. canMatch also works with non-lazy routes and integrates cleanly with the functional guard style.Resolve: Pre-fetching Data
A resolver pre-fetches data before the component renders. The user stays on the current page (with a loading indicator) until the data is ready, then Angular activates the route.
// user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn, ActivatedRouteSnapshot } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';
export const userResolver: ResolveFn<User> = (route: ActivatedRouteSnapshot) => {
const id = route.paramMap.get('id')!;
return inject(UserService).getUser(+id);
// Returns an Observable — Angular subscribes and waits for it
};
// app.routes.ts
{
path: 'users/:id',
component: UserDetailComponent,
resolve: { user: userResolver },
}
// user-detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({ standalone: true, template: `<h1>{{ user.name }}</h1>` })
export class UserDetailComponent {
private route = inject(ActivatedRoute);
user = this.route.snapshot.data['user']; // resolved before component renders
}Async Guards (Observable/Promise)
Guards can return Observable<boolean | UrlTree> or Promise<boolean | UrlTree> for asynchronous checks — token verification, permission API calls, etc.
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
import { map, take } from 'rxjs/operators';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isAuthenticated$.pipe( // Observable<boolean>
take(1),
map(isAuth => isAuth || router.createUrlTree(['/login']))
);
};take(1) or first() when returning an Observable from a guard. Angular will wait indefinitely if the observable never completes, blocking navigation forever.Class-Based Guards (Legacy)
Older codebases use class-based guards that implement CanActivate, CanDeactivate, etc. They still work in modern Angular but are not recommended for new code.
// Class-based guard (legacy style — still supported)
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 auth: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (this.auth.isLoggedIn()) return true;
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
}
}Combining Guards
// Multiple guards — ALL must return true for navigation to proceed
{
path: 'finance/reports',
component: FinanceReportsComponent,
canActivate: [authGuard, roleGuard, featureFlagGuard],
data: { role: 'FINANCE_MANAGER', feature: 'advanced-reports' },
}
// Guard order matters — they run left to right
// If authGuard redirects to /login, roleGuard never runsTesting Guards
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { authGuard } from './auth.guard';
import { AuthService } from './auth.service';
describe('authGuard', () => {
let authService: jasmine.SpyObj<AuthService>;
let router: Router;
beforeEach(() => {
authService = jasmine.createSpyObj('AuthService', ['isLoggedIn']);
TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [{ provide: AuthService, useValue: authService }],
});
router = TestBed.inject(Router);
});
it('allows navigation when logged in', () => {
authService.isLoggedIn.and.returnValue(true);
const result = TestBed.runInInjectionContext(() =>
authGuard({} as any, { url: '/dashboard' } as any)
);
expect(result).toBe(true);
});
it('redirects to login when not logged in', () => {
authService.isLoggedIn.and.returnValue(false);
const result = TestBed.runInInjectionContext(() =>
authGuard({} as any, { url: '/dashboard' } as any)
);
expect(result).toEqual(router.createUrlTree(['/login'], { queryParams: { returnUrl: '/dashboard' } }));
});
});Guard Best Practices
Use functional guards (plain functions) for all new code — they are simpler and more testable
Return a UrlTree for redirects rather than calling router.navigate() — it is cleaner and composable
Use canMatch instead of canLoad to prevent downloading lazy chunks for unauthorised users
Apply canActivateChild to a parent route to protect all children in one place
Keep guards thin — delegate complex logic to injectable services
Always use take(1)/first() on Observables to ensure they complete
Test guards with TestBed.runInInjectionContext() to properly inject dependencies
Summary: Route guards are functions that run before Angular activates, loads, or leaves a route. Use canActivate to protect routes, canDeactivate to prevent accidental navigation, canMatch to block lazy chunk downloads, and resolve to pre-fetch data. The functional guard style (Angular 14.2+) is the recommended approach for all new code.