Guards & Resolvers
Route Guards
Protect routes from unauthorized access.
CanActivate
- Guard
- Route Config
- Auth Service
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
// Redirect to login
return router.createUrlTree(['/login']);
};
import { Routes } from '@angular/router';
import { authGuard } from './auth.guard';
export const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard] // ← Guard applied here
},
{
path: 'login',
component: LoginComponent
}
];
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
private loggedIn = signal(false);
isLoggedIn(): boolean {
return this.loggedIn();
}
login() {
this.loggedIn.set(true);
}
logout() {
this.loggedIn.set(false);
}
}
CanActivateChild
- Guard
- Route Config
export const adminGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
return authService.hasRole('admin');
};
export const routes: Routes = [
{
path: 'admin',
canActivateChild: [adminGuard], // ← Applied to all children
children: [
{ path: 'users', component: UsersComponent },
{ path: 'settings', component: SettingsComponent }
]
}
];
CanDeactivate
- Guard
- Component
- Route Config
export interface CanComponentDeactivate {
canDeactivate: () => boolean | Observable<boolean>;
}
export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = (
component
) => {
return component.canDeactivate ? component.canDeactivate() : true;
};
import { Component } from '@angular/core';
@Component({
selector: 'app-form',
standalone: true,
template: `
<form>
<input [(ngModel)]="data" (input)="hasUnsavedChanges = true">
<button (click)="save()">Save</button>
</form>
`
})
export class FormComponent implements CanComponentDeactivate {
data = '';
hasUnsavedChanges = false;
canDeactivate(): boolean {
if (this.hasUnsavedChanges) {
return confirm('You have unsaved changes. Leave anyway?');
}
return true;
}
save() {
this.hasUnsavedChanges = false;
// Save logic
}
}
{
path: 'edit',
component: FormComponent,
canDeactivate: [canDeactivateGuard]
}
CanMatch
- Guard
- Route Config
export const featureGuard: CanMatchFn = (route, segments) => {
const featureService = inject(FeatureService);
return featureService.isFeatureEnabled('newFeature');
};
export const routes: Routes = [
{
path: 'feature',
canMatch: [featureGuard], // ← If true, use this route
loadComponent: () => import('./new-feature.component')
.then(m => m.NewFeatureComponent)
},
{
path: 'feature', // ← Fallback if canMatch fails
loadComponent: () => import('./old-feature.component')
.then(m => m.OldFeatureComponent)
}
];
Resolvers
Pre-fetch data before activating route.
Basic Resolver
- Resolver
- Route Config
- Component
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from './user.service';
export const userResolver: ResolveFn<User> = (route, state) => {
const userService = inject(UserService);
const id = route.paramMap.get('id')!;
return userService.getUser(id);
};
export const routes: Routes = [
{
path: 'user/:id',
component: UserDetailComponent,
resolve: { user: userResolver } // ← Data available as 'user'
}
];
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-user-detail',
standalone: true,
template: `
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
`
})
export class UserDetailComponent {
route = inject(ActivatedRoute);
// Get resolved data from snapshot
user = this.route.snapshot.data['user'] as User;
// Or reactively
ngOnInit() {
this.route.data.subscribe(data => {
this.user = data['user'];
});
}
}
Multiple Resolvers
- Resolvers
- Route Config
- Component
export const userResolver: ResolveFn<User> = (route) => {
return inject(UserService).getUser(route.params['id']);
};
export const postsResolver: ResolveFn<Post[]> = (route) => {
return inject(PostService).getPosts(route.params['id']);
};
export const routes: Routes = [
{
path: 'user/:id',
component: UserProfileComponent,
resolve: {
user: userResolver, // ← Available as data['user']
posts: postsResolver // ← Available as data['posts']
}
}
];
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
<h2>{{ user.name }}</h2>
<div *ngFor="let post of posts">
<p>{{ post.title }}</p>
</div>
`
})
export class UserProfileComponent {
route = inject(ActivatedRoute);
ngOnInit() {
const data = this.route.snapshot.data;
this.user = data['user'];
this.posts = data['posts'];
}
}
Error Handling in Resolver
- Resolver
- Route Config
import { catchError, EMPTY } from 'rxjs';
export const userResolver: ResolveFn<User> = (route) => {
const userService = inject(UserService);
const router = inject(Router);
return userService.getUser(route.params['id']).pipe(
catchError(error => {
console.error('Failed to load user', error);
router.navigate(['/not-found']);
return EMPTY; // Don't navigate to route
})
);
};
{
path: 'user/:id',
component: UserDetailComponent,
resolve: { user: userResolver } // ← Will redirect on error
}
Combining Guards and Resolvers
- Guards
- Resolvers
- Route Config
export const authGuard: CanActivateFn = (route, state) => {
return inject(AuthService).isLoggedIn();
};
export const unsavedChangesGuard: CanDeactivateFn<FormComponent> = (component) => {
return component.canDeactivate();
};
export const userResolver: ResolveFn<User> = (route) => {
return inject(UserService).getUser(route.params['id']);
};
export const settingsResolver: ResolveFn<Settings> = (route) => {
return inject(SettingsService).getSettings();
};
export const routes: Routes = [
{
path: 'profile/:id',
component: ProfileComponent,
canActivate: [authGuard], // ← Check auth first
canDeactivate: [unsavedChangesGuard], // ← Check before leaving
resolve: { // ← Load data
user: userResolver,
settings: settingsResolver
}
}
];
Role-based Guard
- Guard Factory
- Route Config
export function roleGuard(roles: string[]): CanActivateFn {
return (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
const userRoles = authService.getUserRoles();
const hasRole = roles.some(role => userRoles.includes(role));
if (hasRole) {
return true;
}
return router.createUrlTree(['/unauthorized']);
};
}
export const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [roleGuard(['admin', 'superuser'])] // ← Pass roles
},
{
path: 'moderator',
component: ModeratorComponent,
canActivate: [roleGuard(['moderator', 'admin'])]
}
];
Async Guard
- Guard
- Route Config
export const permissionGuard: CanActivateFn = (route, state) => {
const permissionService = inject(PermissionService);
const router = inject(Router);
return permissionService.checkPermission(route.data['permission']).pipe(
map(hasPermission => {
if (hasPermission) {
return true;
}
return router.createUrlTree(['/forbidden']);
})
);
};
{
path: 'admin',
component: AdminComponent,
canActivate: [permissionGuard],
data: { permission: 'admin.access' } // ← Passed to guard
}
Guard with Redirect
- Guard
- Login Component
export const loginGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
// Redirect to login with return URL
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
@Component({
selector: 'app-login',
standalone: true,
template: `
<button (click)="login()">Login</button>
`
})
export class LoginComponent {
route = inject(ActivatedRoute);
router = inject(Router);
authService = inject(AuthService);
login() {
this.authService.login();
// Redirect to original URL
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.router.navigateByUrl(returnUrl);
}
}
Best Practices
- Use guards for authentication/authorization
- Use resolvers to pre-fetch critical data
- Always handle errors in resolvers
- Return
UrlTreeinstead ofrouter.navigate()in guards - Keep guards simple and focused
- Use
canMatchfor feature flags - Use
canDeactivatefor unsaved changes - Combine multiple guards when needed
- Consider loading states for resolvers