Route guards, resolvers and redirects

Angular · Advanced · 12 min

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, a UrlTree (redirect, e.g. inject(Router).createUrlTree(['/login'])), or a RedirectCommand; 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
  • canMatch also prevents lazy code from loading; canDeactivate guards 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.

Check yourself

  1. Which guard prevents a lazy-loaded admin bundle from even being downloaded for non-admins?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/routing/route-guards

Builds on

Unlocks

Nothing builds on this yet.