Route Guards
Route guards are functions or classes that control navigation in Angular applications. They can prevent navigation, redirect to different routes, or allow navigation to proceed based on custom logic.
Import
import {
CanActivateFn ,
CanActivateChildFn ,
CanDeactivateFn ,
CanMatchFn ,
CanLoadFn ,
GuardResult
} from '@angular/router' ;
Guard Types
Angular provides several types of guards for different navigation scenarios:
CanActivate Controls if a route can be activated
CanActivateChild Controls if child routes can be activated
CanDeactivate Controls if a route can be deactivated
CanMatch Controls if a route can be matched
Modern Angular applications use functional guards (functions) instead of class-based guards. Functional guards are more concise and leverage dependency injection through the inject() function.
Guard Result Types
GuardResult
type GuardResult = boolean | UrlTree | RedirectCommand ;
All guards return a GuardResult, which can be:
true - Allow navigation to proceed
false - Cancel navigation
UrlTree - Redirect to a different URL
RedirectCommand - Redirect with navigation options
MaybeAsync
type MaybeAsync < T > = T | Observable < T > | Promise < T >;
Guards can return synchronous values or async values (Observable or Promise).
CanActivate
CanActivateFn
type CanActivateFn = (
route : ActivatedRouteSnapshot ,
state : RouterStateSnapshot
) => MaybeAsync < GuardResult >;
Determines if a route can be activated. Used to protect routes that require authentication, authorization, or other preconditions.
Parameters:
route
ActivatedRouteSnapshot
required
The activated route snapshot containing route parameters, data, and configuration.
state
RouterStateSnapshot
required
The router state snapshot containing the target URL and routing tree.
Returns: true to allow, false to cancel, or UrlTree/RedirectCommand to redirect.
Examples:
import { inject } from '@angular/core' ;
import { CanActivateFn , Router } from '@angular/router' ;
import { AuthService } from './auth.service' ;
// Basic authentication guard
export const authGuard : CanActivateFn = ( route , state ) => {
const authService = inject ( AuthService );
const router = inject ( Router );
if ( authService . isLoggedIn ()) {
return true ;
}
// Redirect to login page with return URL
return router . createUrlTree ([ '/login' ], {
queryParams: { returnUrl: state . url }
});
};
// Role-based authorization guard
export const adminGuard : CanActivateFn = ( route , state ) => {
const authService = inject ( AuthService );
const router = inject ( Router );
if ( authService . hasRole ( 'admin' )) {
return true ;
}
// Redirect to unauthorized page
return router . createUrlTree ([ '/unauthorized' ]);
};
// Async guard with Observable
export const permissionGuard : CanActivateFn = ( route , state ) => {
const authService = inject ( AuthService );
const requiredPermission = route . data [ 'permission' ];
return authService . hasPermission ( requiredPermission ). pipe (
map ( hasPermission => hasPermission || router . createUrlTree ([ '/unauthorized' ]))
);
};
// Guard with route parameter validation
export const userAccessGuard : CanActivateFn = ( route , state ) => {
const authService = inject ( AuthService );
const userId = route . params [ 'id' ];
const currentUserId = authService . getCurrentUserId ();
// Users can only access their own profile
if ( userId === currentUserId || authService . hasRole ( 'admin' )) {
return true ;
}
return false ;
};
Route Configuration:
import { Routes } from '@angular/router' ;
const routes : Routes = [
{
path: 'dashboard' ,
component: DashboardComponent ,
canActivate: [ authGuard ]
},
{
path: 'admin' ,
component: AdminComponent ,
canActivate: [ authGuard , adminGuard ] // Multiple guards
},
{
path: 'users/:id' ,
component: UserProfileComponent ,
canActivate: [ authGuard , userAccessGuard ],
data: { permission: 'users:read' }
}
];
CanActivate Interface (Deprecated)
interface CanActivate {
canActivate (
route : ActivatedRouteSnapshot ,
state : RouterStateSnapshot
) : MaybeAsync < GuardResult >;
}
Class-based guards using CanActivate interface are deprecated. Use functional guards (CanActivateFn) instead.
CanActivateChild
CanActivateChildFn
type CanActivateChildFn = (
childRoute : ActivatedRouteSnapshot ,
state : RouterStateSnapshot
) => MaybeAsync < GuardResult >;
Determines if child routes can be activated. Applied to parent routes to protect all child routes.
Example:
import { inject } from '@angular/core' ;
import { CanActivateChildFn , Router } from '@angular/router' ;
import { AuthService } from './auth.service' ;
// Protect all child routes
export const adminChildGuard : CanActivateChildFn = ( route , state ) => {
const authService = inject ( AuthService );
const router = inject ( Router );
if ( authService . hasRole ( 'admin' )) {
return true ;
}
return router . createUrlTree ([ '/unauthorized' ]);
};
// Check specific permissions for child routes
export const moduleAccessGuard : CanActivateChildFn = ( route , state ) => {
const authService = inject ( AuthService );
const requiredModule = route . data [ 'module' ];
if ( authService . hasModuleAccess ( requiredModule )) {
return true ;
}
return false ;
};
Route Configuration:
const routes : Routes = [
{
path: 'admin' ,
component: AdminLayoutComponent ,
canActivateChild: [ adminChildGuard ], // Applied to all children
children: [
{ path: 'users' , component: UsersComponent },
{ path: 'settings' , component: SettingsComponent },
{ path: 'reports' , component: ReportsComponent }
]
}
];
CanDeactivate
CanDeactivateFn
type CanDeactivateFn < T > = (
component : T ,
currentRoute : ActivatedRouteSnapshot ,
currentState : RouterStateSnapshot ,
nextState : RouterStateSnapshot
) => MaybeAsync < GuardResult >;
Determines if a route can be deactivated. Commonly used to prevent navigation away from pages with unsaved changes.
Parameters:
The component instance being deactivated.
currentRoute
ActivatedRouteSnapshot
required
The current activated route.
currentState
RouterStateSnapshot
required
The current router state.
nextState
RouterStateSnapshot
required
The target router state.
Examples:
import { CanDeactivateFn } from '@angular/router' ;
// Define interface for components with unsaved changes
export interface CanComponentDeactivate {
canDeactivate : () => boolean | Promise < boolean >;
}
// Guard for unsaved changes
export const unsavedChangesGuard : CanDeactivateFn < CanComponentDeactivate > = (
component ,
currentRoute ,
currentState ,
nextState
) => {
// If component implements canDeactivate, call it
if ( component . canDeactivate ) {
return component . canDeactivate ();
}
return true ;
};
// Guard with user confirmation dialog
export const confirmLeaveGuard : CanDeactivateFn < any > = (
component ,
currentRoute ,
currentState ,
nextState
) => {
if ( component . hasUnsavedChanges ?.()) {
return confirm ( 'You have unsaved changes. Do you really want to leave?' );
}
return true ;
};
// Async guard with custom dialog service
export const asyncConfirmGuard : CanDeactivateFn < any > = async (
component ,
currentRoute ,
currentState ,
nextState
) => {
if ( ! component . hasUnsavedChanges ?.()) {
return true ;
}
const dialogService = inject ( DialogService );
const result = await dialogService . confirm ({
title: 'Unsaved Changes' ,
message: 'You have unsaved changes. Do you want to discard them?' ,
confirmText: 'Discard' ,
cancelText: 'Stay'
});
return result ;
};
Component Implementation:
import { Component } from '@angular/core' ;
import { CanComponentDeactivate } from './guards/unsaved-changes.guard' ;
@ Component ({
selector: 'app-edit-form' ,
template: `
<form [formGroup]="form">
<!-- form fields -->
</form>
`
})
export class EditFormComponent implements CanComponentDeactivate {
form : FormGroup ;
private savedData : any ;
canDeactivate () : boolean {
// Check if form has unsaved changes
if ( this . form . dirty ) {
return confirm ( 'You have unsaved changes. Are you sure you want to leave?' );
}
return true ;
}
hasUnsavedChanges () : boolean {
return this . form . dirty ;
}
}
Route Configuration:
const routes : Routes = [
{
path: 'edit' ,
component: EditFormComponent ,
canDeactivate: [ unsavedChangesGuard ]
}
];
CanMatch
CanMatchFn
type CanMatchFn = (
route : Route ,
segments : UrlSegment [],
currentSnapshot ?: PartialMatchRouteSnapshot
) => MaybeAsync < GuardResult >;
Determines if a route configuration can be matched. Used for feature flags, conditional routing, and A/B testing.
Parameters:
The route configuration being evaluated.
The URL segments that have not been consumed yet.
currentSnapshot
PartialMatchRouteSnapshot
The current route snapshot up to this point in matching (optional for backward compatibility).
Examples:
import { inject } from '@angular/core' ;
import { CanMatchFn } from '@angular/router' ;
import { FeatureFlagService } from './feature-flag.service' ;
// Feature flag guard
export const featureGuard : CanMatchFn = ( route , segments ) => {
const featureFlags = inject ( FeatureFlagService );
const featureName = route . data ?.[ 'feature' ];
if ( ! featureName ) {
return true ;
}
return featureFlags . isEnabled ( featureName );
};
// Environment-based routing
export const productionOnlyGuard : CanMatchFn = () => {
return environment . production ;
};
// User segment guard for A/B testing
export const betaUserGuard : CanMatchFn = ( route , segments ) => {
const userService = inject ( UserService );
return userService . isBetaUser ();
};
// Mobile/Desktop conditional routing
export const mobileGuard : CanMatchFn = () => {
const platformService = inject ( PlatformService );
return platformService . isMobile ();
};
Route Configuration:
const routes : Routes = [
// New feature behind feature flag
{
path: 'dashboard' ,
component: NewDashboardComponent ,
canMatch: [ featureGuard ],
data: { feature: 'new-dashboard' }
},
// Fallback to old dashboard if feature disabled
{
path: 'dashboard' ,
component: OldDashboardComponent
},
// A/B testing routes
{
path: 'home' ,
component: HomeVariantAComponent ,
canMatch: [ betaUserGuard ]
},
{
path: 'home' ,
component: HomeVariantBComponent
},
// Platform-specific routes
{
path: 'editor' ,
component: MobileEditorComponent ,
canMatch: [ mobileGuard ]
},
{
path: 'editor' ,
component: DesktopEditorComponent
}
];
Unlike CanActivate, CanMatch prevents the route from being matched at all. If a canMatch guard returns false, the router continues to the next route configuration. This is ideal for feature flags and conditional routing.
CanLoad (Deprecated)
CanLoadFn
type CanLoadFn = (
route : Route ,
segments : UrlSegment []
) => MaybeAsync < GuardResult >;
CanLoad is deprecated in favor of CanMatch. Use CanMatchFn for all new code.
RedirectCommand
RedirectCommand Class
class RedirectCommand {
constructor (
readonly redirectTo : UrlTree ,
readonly navigationBehaviorOptions ?: NavigationBehaviorOptions
);
}
Used in guards and resolvers to redirect with specific navigation options.
Example:
import { inject } from '@angular/core' ;
import { CanActivateFn , Router , RedirectCommand } from '@angular/router' ;
import { AuthService } from './auth.service' ;
export const authWithRedirectGuard : CanActivateFn = ( route , state ) => {
const authService = inject ( AuthService );
const router = inject ( Router );
if ( authService . isLoggedIn ()) {
return true ;
}
const loginUrl = router . parseUrl ( '/login' );
return new RedirectCommand ( loginUrl , {
skipLocationChange: true , // Don't update browser URL
state: { returnUrl: state . url } // Pass data to login page
});
};
Guard Execution Order
When multiple guards are present, they execute in the following order:
Route matching : canMatch guards
Activation : canActivate guards (from parent to child)
Child activation : canActivateChild guards (from parent to child)
Deactivation : canDeactivate guards (when leaving route)
Example:
const routes : Routes = [
{
path: 'admin' ,
component: AdminLayoutComponent ,
canMatch: [ featureGuard ], // 1. Check feature flag
canActivate: [ authGuard ], // 2. Check authentication
canActivateChild: [ adminGuard ], // 3. Check admin role for children
children: [
{
path: 'settings' ,
component: SettingsComponent ,
canActivate: [ settingsAccessGuard ], // 4. Additional guard for this child
canDeactivate: [ unsavedChangesGuard ] // 5. Check when leaving
}
]
}
];
Common Guard Patterns
Combining Multiple Guards
const routes : Routes = [
{
path: 'admin' ,
component: AdminComponent ,
canActivate: [
authGuard , // Must be logged in
adminGuard , // Must be admin
licenseGuard // Must have valid license
]
}
];
Reusable Permission Guard
import { inject } from '@angular/core' ;
import { CanActivateFn } from '@angular/router' ;
import { AuthService } from './auth.service' ;
export const permissionGuard = ( permission : string ) : CanActivateFn => {
return ( route , state ) => {
const authService = inject ( AuthService );
return authService . hasPermission ( permission );
};
};
// Usage
const routes : Routes = [
{
path: 'users' ,
component: UsersComponent ,
canActivate: [ permissionGuard ( 'users:read' )]
},
{
path: 'users/create' ,
component: CreateUserComponent ,
canActivate: [ permissionGuard ( 'users:create' )]
}
];
Guard with Loading State
import { inject } from '@angular/core' ;
import { CanActivateFn } from '@angular/router' ;
import { LoadingService } from './loading.service' ;
import { AuthService } from './auth.service' ;
import { tap , finalize } from 'rxjs/operators' ;
export const authWithLoadingGuard : CanActivateFn = ( route , state ) => {
const authService = inject ( AuthService );
const loadingService = inject ( LoadingService );
loadingService . show ();
return authService . checkAuth (). pipe (
tap ( isAuthenticated => {
if ( ! isAuthenticated ) {
// Handle redirect
}
}),
finalize (() => loadingService . hide ())
);
};
Guard with Error Handling
import { inject } from '@angular/core' ;
import { CanActivateFn , Router } from '@angular/router' ;
import { AuthService } from './auth.service' ;
import { catchError , of } from 'rxjs' ;
export const safeAuthGuard : CanActivateFn = ( route , state ) => {
const authService = inject ( AuthService );
const router = inject ( Router );
return authService . checkAuth (). pipe (
catchError ( error => {
console . error ( 'Auth check failed:' , error );
// Redirect to error page or login
return of ( router . createUrlTree ([ '/error' ]));
})
);
};
Testing Guards
import { TestBed } from '@angular/core/testing' ;
import { Router } from '@angular/router' ;
import { authGuard } from './auth.guard' ;
import { AuthService } from './auth.service' ;
describe ( 'authGuard' , () => {
let authService : jasmine . SpyObj < AuthService >;
let router : jasmine . SpyObj < Router >;
beforeEach (() => {
const authServiceSpy = jasmine . createSpyObj ( 'AuthService' , [ 'isLoggedIn' ]);
const routerSpy = jasmine . createSpyObj ( 'Router' , [ 'createUrlTree' ]);
TestBed . configureTestingModule ({
providers: [
{ provide: AuthService , useValue: authServiceSpy },
{ provide: Router , useValue: routerSpy }
]
});
authService = TestBed . inject ( AuthService ) as jasmine . SpyObj < AuthService >;
router = TestBed . inject ( Router ) as jasmine . SpyObj < Router >;
});
it ( 'should allow access when user is logged in' , () => {
authService . isLoggedIn . and . returnValue ( true );
const result = TestBed . runInInjectionContext (() =>
authGuard ( null as any , null as any )
);
expect ( result ). toBe ( true );
});
it ( 'should redirect to login when user is not logged in' , () => {
authService . isLoggedIn . and . returnValue ( false );
const mockUrlTree = {} as any ;
router . createUrlTree . and . returnValue ( mockUrlTree );
const result = TestBed . runInInjectionContext (() =>
authGuard ( null as any , { url: '/admin' } as any )
);
expect ( result ). toBe ( mockUrlTree );
expect ( router . createUrlTree ). toHaveBeenCalledWith (
[ '/login' ],
{ queryParams: { returnUrl: '/admin' } }
);
});
});
See Also
Router Class Router service API
Route Configuration Configure routes with guards
Route Guards Guide Complete guide to route guards
Dependency Injection Using inject() in guards