Route guards participate in navigation decisions; they do not secure a server API. Learn functional guards, UrlTree redirects, unsaved-change checks, resolvers, and the difference between authorization and code-loading boundaries.
A route guard participates in client navigation and returns boolean, UrlTree, RedirectCommand, Promise, or Observable. Functional guards can inject services directly and are the clearest default for new routes.
Use canActivate for entry policy, canActivateChild for a child tree, canMatch to choose whether a route configuration matches, and canDeactivate to ask whether the current component may be left. Use a resolver for required route data.
A guard is not security enforcement because it runs in user-controlled browser code. The server must authenticate and authorize every protected operation.
| Guard | Purpose |
|---|---|
| canActivate | Decides if a route can be activated (navigated to) |
| canActivateChild | Decides if child routes can be activated |
| canDeactivate | Decides if a route can be left (e.g. unsaved changes) |
| canMatch | Decides if a route definition should be matched at all |
| resolve | Pre-fetches data before the route activates |
Use canActivate when entry to a matched route depends on application state such as a signed-in session or completed onboarding. Return a redirect result instead of returning false and starting a second navigation imperatively.
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
// Functional guard with dependencies resolved from the route injection context
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
// Redirect to login, preserving the intended URL
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// auth.service.ts (simplified)
// @Injectable({ providedIn: 'root' })
// export class AuthService {
// private loggedIn = signal(false);
// isLoggedIn() { return this.loggedIn(); }
// login() { this.loggedIn.set(true); }
// logout() { this.loggedIn.set(false); }
// }
import { Routes } from '@angular/router';
import { authGuard } from './auth.guard';
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './login/login.component';
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard] // protect this route
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
canActivate: [authGuard] // protect entire lazy-loaded section
}
];
Use canMatch when a condition should decide whether the router selects a route definition at all. Angular evaluates it during route recognition, before activation. A true result keeps the route as a candidate; false makes the router continue checking later route definitions that match the same URL.
A CanMatchFn receives the Route configuration, the remaining UrlSegment array, and the PartialMatchRouteSnapshot built so far. It can return boolean, UrlTree, RedirectCommand, Promise, or Observable. Return a UrlTree or RedirectCommand when the user must be redirected; returning false means "try another route", not "go to login".
canMatch is useful for feature flags, staged rollouts, role-specific screens at one URL, and lazy routes that should not be selected for the current user. It replaces the deprecated canLoad guard. It can stop lazy code from loading as part of that navigation, but it is still browser-side routing logic and never replaces server authorization.
import { inject } from '@angular/core';
import { CanMatchFn } from '@angular/router';
import { FeatureFlagsService } from './feature-flags.service';
export const featureFlagGuard: CanMatchFn = (
route,
_segments,
_currentSnapshot
) => {
const flags = inject(FeatureFlagsService);
const feature = route.data?.['feature'] as string;
// false tells the router to try the next matching route.
return flags.isEnabled(feature);
};
import { Routes } from '@angular/router';
import { featureFlagGuard } from './feature-flag.guard';
export const routes: Routes = [
{
path: 'dashboard',
data: { feature: 'new-dashboard' },
canMatch: [featureFlagGuard],
loadComponent: () =>
import('./new-dashboard.component')
.then(m => m.NewDashboardComponent)
},
{
// Used when featureFlagGuard returns false.
path: 'dashboard',
loadComponent: () =>
import('./classic-dashboard.component')
.then(m => m.ClassicDashboardComponent)
}
];
Use canDeactivate to warn before Angular navigation leaves a form with unsaved changes. Keep the component contract small, such as hasUnsavedChanges(), so the guard does not depend on one form implementation.
Only prompt when saved data differs from the current form, and skip the prompt after a successful save. Browser beforeunload is a separate boundary for closing or refreshing the tab; a router guard only sees Angular navigation.
import { CanDeactivateFn } from '@angular/router';
// Define an interface for components that can be guarded
export interface CanComponentDeactivate {
canDeactivate: () => boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
if (component.canDeactivate()) {
return true;
}
return confirm('You have unsaved changes. Leave anyway?');
};
import { Component, signal } from '@angular/core';
import { CanComponentDeactivate } from './unsaved-changes.guard';
@Component({
selector: 'app-edit-form',
standalone: true,
template: `
<input [(ngModel)]="name" (ngModelChange)="isDirty.set(true)" />
<button (click)="save()">Save</button>
<p *ngIf="isDirty()">You have unsaved changes</p>
`
})
export class EditFormComponent implements CanComponentDeactivate {
name = '';
isDirty = signal(false);
save() {
// ... save logic
this.isDirty.set(false);
}
canDeactivate(): boolean {
return !this.isDirty();
}
}
A resolver loads required route data before activation. The component can read the resolved value from route data or a configured component input, while optional or progressively loaded data can remain a component-owned loading state.
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { HttpClient } from '@angular/common/http';
export interface User { id: number; name: string; email: string; }
export const userResolver: ResolveFn<User> = (route) => {
const http = inject(HttpClient);
const id = route.paramMap.get('id');
return http.get<User>(`https://api.example.com/users/${id}`);
};
import { Routes } from '@angular/router';
import { userResolver } from './user.resolver';
import { UserDetailComponent } from './user-detail.component';
export const routes: Routes = [
{
path: 'users/:id',
component: UserDetailComponent,
resolve: { user: userResolver }
}
];
// In UserDetailComponent:
// constructor(private route: ActivatedRoute) {}
// ngOnInit() {
// const user = this.route.snapshot.data['user'];
// }
It expresses the redirect as the guard’s result. The router cancels the original navigation and starts the replacement cleanly.
canActivate decides whether the route may activate after route matching and loading have progressed. Use canMatch when unauthorized users should not match that lazy route in the first place; this can prevent the router from selecting and loading that route configuration.
The component’s dirty flag or form state was not reset after a successful save. Set the form pristine or update the signal only after the server confirms persistence; clearing it before the request succeeds can lose the warning when saving fails.
Explore 500+ free tutorials across 20+ languages and frameworks.