What Are Route Guards?
Route guards are functions that control whether a user can navigate to or away from a route. They run before the route is activated and can:
- Prevent unauthorized access
- Redirect to a login page
- Confirm before leaving unsaved changes
- Pre-fetch data before a route loads
Types of Route Guards
| Guard | When It Runs | Use Case |
|---|---|---|
canActivate | Before navigating to a route | Authentication check |
canActivateChild | Before navigating to child routes | Protect all admin children |
canDeactivate | Before leaving a route | Unsaved changes warning |
canMatch | Before loading route config | Feature flags, AB testing |
resolve | Before route activates | Pre-fetch data |
Functional Guards (Modern — Angular 15+)
canActivate — Authentication Guard
typescript
// guards/auth.guard.ts
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
// Redirect to login with return URL
router.navigate(['/login'], {
queryParams: { returnUrl: state.url },
});
return false;
};Usage in Routes
typescript
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard],
},
{
path: 'admin',
component: AdminLayoutComponent,
canActivate: [authGuard, roleGuard],
children: [
{ path: '', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent },
],
},
];Role-Based Guard
typescript
// guards/role.guard.ts
export const roleGuard: CanActivateFn = (route) => {
const authService = inject(AuthService);
const router = inject(Router);
const requiredRoles = route.data['roles'] as string[];
const userRole = authService.getUserRole();
if (requiredRoles.includes(userRole)) {
return true;
}
router.navigate(['/unauthorized']);
return false;
};typescript
// Route with role requirement
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard, roleGuard],
data: { roles: ['admin', 'super-admin'] },
}canDeactivate — Unsaved Changes Guard
Prevent users from accidentally losing unsaved work:
typescript
// guards/unsaved-changes.guard.ts
import { CanDeactivateFn } from '@angular/router';
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (component.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Do you really want to leave?');
}
return true;
};typescript
// The component implements the interface
@Component({ /* ... */ })
export class EditProfileComponent implements HasUnsavedChanges {
originalData = '';
currentData = '';
hasUnsavedChanges(): boolean {
return this.originalData !== this.currentData;
}
}typescript
// Route configuration
{
path: 'profile/edit',
component: EditProfileComponent,
canActivate: [authGuard],
canDeactivate: [unsavedChangesGuard],
}Route Resolvers
Resolvers pre-fetch data before a route activates, so the component receives the data immediately:
Functional Resolver
typescript
// resolvers/user.resolver.ts
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { UserService } from '../services/user.service';
export const userResolver: ResolveFn<User> = (route) => {
const userService = inject(UserService);
const userId = route.params['id'];
return userService.getUserById(userId);
};
export const userListResolver: ResolveFn<User[]> = () => {
return inject(UserService).getUsers();
};Using Resolvers in Routes
typescript
const routes: Routes = [
{
path: 'users',
component: UserListComponent,
resolve: { users: userListResolver },
},
{
path: 'users/:id',
component: UserDetailComponent,
resolve: { user: userResolver },
},
];Accessing Resolved Data
typescript
@Component({
selector: 'app-user-detail',
standalone: true,
template: `
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
`,
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
user!: User;
ngOnInit() {
this.user = this.route.snapshot.data['user'];
// Or reactively
this.route.data.subscribe(data => {
this.user = data['user'];
});
}
}canMatch — Feature Flags
Control whether a route even matches based on conditions:
typescript
export const featureFlagGuard: CanMatchFn = (route) => {
const featureService = inject(FeatureService);
const feature = route.data?.['feature'];
return featureService.isEnabled(feature);
};
const routes: Routes = [
{
path: 'new-dashboard',
component: NewDashboardComponent,
canMatch: [featureFlagGuard],
data: { feature: 'new-dashboard' },
},
{
path: 'new-dashboard',
component: OldDashboardComponent, // Fallback
},
];Guard with Async Logic
Guards can return Observables or Promises:
typescript
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.checkAuth().pipe(
map(isAuthenticated => {
if (isAuthenticated) return true;
return router.createUrlTree(['/login']);
}),
catchError(() => {
return of(router.createUrlTree(['/login']));
}),
);
};Complete Guard Example
typescript
// A comprehensive auth guard
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
// Check if logged in
if (!authService.isLoggedIn()) {
router.navigate(['/login'], {
queryParams: { returnUrl: state.url },
});
return false;
}
// Check if token is expired
if (authService.isTokenExpired()) {
authService.logout();
router.navigate(['/login'], {
queryParams: { reason: 'expired' },
});
return false;
}
// Check role requirements
const requiredRoles = route.data['roles'] as string[] | undefined;
if (requiredRoles && !requiredRoles.includes(authService.getUserRole())) {
router.navigate(['/unauthorized']);
return false;
}
return true;
};Best Practices
- Use functional guards — they are simpler and tree-shakable
- Combine guards with the
canActivatearray for layered security - Return
UrlTreeinstead of callingrouter.navigate()for cleaner redirects - Keep guards focused — one guard per concern (auth, roles, permissions)
- Use resolvers to prevent "loading" flickers on route change
Next Steps
For large applications, loading everything upfront is inefficient. Next, we'll explore lazy loading — how to split your app into chunks that load on demand for better performance.