Authentication, permissions, unsaved-change prompts and data preloading are built on these router hooks.
The router can run functions before activating a route. Modern guards are plain functions that can inject() services.
- `canActivate`: may the user enter this route? Return
true,false, aUrlTree(redirect, e.g.inject(Router).createUrlTree(['/login'])), or aRedirectCommand; Promises and Observables of these work too. - `canMatch`: should this route definition even be considered? If false, the router tries the next matching route, and lazy-loaded code isn't downloaded. Good for role-based alternatives (admin vs user dashboard at the same path).
- `canActivateChild`: guard every child route.
- `canDeactivate`: may the user leave? Receives the component instance, used for "unsaved changes" confirms.
Resolvers (ResolveFn<T>) fetch data before the route activates; the result is available as route data, and with component input binding as an input of the same name. Resolvers delay navigation until data arrives, so use them for small, essential data; for most pages, loading in the component with a resource gives faster perceived navigation.
Guards run on navigation, which is a UX layer only: always enforce authorisation on the server, since client code can be bypassed.
Keep guards small and reusable, e.g. a roleGuard('admin') factory that returns a CanActivateFn.
Example
import { inject } from '@angular/core';
import { CanActivateFn, CanDeactivateFn, ResolveFn, Router, Routes } from '@angular/router';
import { HttpClient } from '@angular/common/http';
declare class Auth { isLoggedIn(): boolean; hasRole(r: string): boolean } // your service
type Project = { id: string; name: string };
export const authGuard: CanActivateFn = (route, state) =>
inject(Auth).isLoggedIn() ||
inject(Router).createUrlTree(['/login'], { queryParams: { next: state.url } });
export const roleGuard = (role: string): CanActivateFn => () => inject(Auth).hasRole(role);
export const unsavedGuard: CanDeactivateFn<{ dirty(): boolean }> = (cmp) =>
!cmp.dirty() || confirm('Discard unsaved changes?');
export const projectResolver: ResolveFn<Project> = (route) =>
inject(HttpClient).get<Project>(`/api/projects/${route.paramMap.get('id')}`);
export const routes: Routes = [
{ path: 'projects/:id', canActivate: [authGuard], canDeactivate: [unsavedGuard],
resolve: { project: projectResolver }, // -> project input
loadComponent: () => import('./project').then((m) => m.ProjectPage) },
{ path: 'admin', canMatch: [roleGuard('admin')],
loadChildren: () => import('./admin/routes').then((m) => m.adminRoutes) },
];Remember
- Guards are functions returning boolean, UrlTree/RedirectCommand, or async versions
canMatchalso prevents lazy code from loading;canDeactivateguards leaving- Resolvers preload data but delay navigation
- Client guards are UX; enforce security on the server
Watch out for
- Treating route guards as security; anyone can call your API directly.