skip to content

Functional Route Guards

Protecting routes with CanActivateFn, CanActivateChildFn, CanDeactivateFn and CanMatchFn, returning a boolean, UrlTree or RedirectCommand. Interviewers probe auth redirects and unsaved-change prompts.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In Angular, how do you write a functional canActivate guard that lets only admins into an /admin route and redirects everyone else?

level: juniorimportance: must knowfreq 80%

answer

  1. a plain function, not a class
  2. the CanActivateFn signature
  3. inject() the auth service inside it
  4. return a UrlTree instead of false

basics

~10 s

Write a CanActivateFn that injects the auth service, returns true for admins and returns a UrlTree (router.createUrlTree(['/login'])) or a RedirectCommand for everyone else, then list it in the route's canActivate array.

solid answer

~40 s

A functional guard is a plain function typed `CanActivateFn`: `(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync<GuardResult>`, where `GuardResult` is `boolean | UrlTree | RedirectCommand` and `MaybeAsync` also allows an `Observable` or `Promise`. Inside it I call `inject(AuthService)` and `inject(Router)`, return `true` for an admin, and return `router.createUrlTree(['/login'])` for a signed-out user or `router.createUrlTree(['/forbidden'])` for a signed-in non-admin. I attach it with `canActivate: [adminGuard]` on the `admin` route. Returning a `UrlTree` makes the router cancel the current navigation and start a new one to that tree, which is cleaner than returning `false` and calling `navigate()` myself. For async checks the router takes the first emitted value. It is a UX gate only: the server must still reject non-admin API calls.

code

ts · 24 lines
ts
import { inject } from '@angular/core';
import { CanActivateFn, Router, Routes } from '@angular/router';
import { AuthService } from './auth.service';

export const adminGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (!auth.isSignedIn()) {
    return router.createUrlTree(['/login']);
  }
  if (!auth.hasRole('admin')) {
    return router.createUrlTree(['/forbidden']);
  }
  return true;
};

export const routes: Routes = [
  {
    path: 'admin',
    canActivate: [adminGuard],
    loadComponent: () => import('./admin/admin-shell').then((m) => m.AdminShell),
  },
];

go deeper

for a junior

Recall the CanActivateFn signature, that it returns true, false, a UrlTree or a RedirectCommand, and that it is attached with canActivate: [guard] on the route.

for a middle

Explain why a returned UrlTree beats navigate() plus false, how the router treats Observable and Promise results, and why inject() works inside the function.

for a senior

Show you would split signed-out and forbidden redirects, gate a whole admin section once at the parent, and keep server-side authorization as the real control.

for a principal

Frame guards as a navigation UX layer over server authorization, and set team conventions for where guards live and how role checks are composed.

## What a functional route guard is In Angular's router (`@angular/router`), a **route guard** is code the router runs during a navigation to decide whether that navigation may continue. The current recommended form is a **functional guard**: a plain exported function whose type is one of the guard function types. For "may the user enter this route?" the type is `CanActivateFn`: ```ts type CanActivateFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, ) => MaybeAsync<GuardResult>; ``` - `route` is the `ActivatedRouteSnapshot` of the route being activated: its params, query params, data and route config. - `state` is the `RouterStateSnapshot` of the whole target state; `state.url` is the URL the user is trying to reach. - `GuardResult` is `boolean | UrlTree | RedirectCommand`. - `MaybeAsync<T>` is `T | Observable<T> | Promise<T>`, so the guard may answer synchronously or later. The guard runs inside an **injection context** built from the route's environment injector, which is why it can call `inject(AuthService)` directly instead of receiving services through a constructor. ## The admin-only guard, step by step 1. Inject what the decision needs: an auth service and the `Router`. 2. If the user is an admin, return `true`: the navigation continues to the next check. 3. If the user is not signed in, return `router.createUrlTree(['/login'])`. 4. If the user is signed in but not an admin, return a tree to a `/forbidden` page instead, so a signed-in user is not sent back to a sign-in form they cannot use. 5. Attach the function to the route with `canActivate: [adminGuard]`. The array can hold several guards. ## What each result does | Result | Effect on the navigation | |---|---| | `true` | The navigation continues; other guards and then resolvers still run | | `false` | The navigation is cancelled and a `NavigationCancel` event is emitted | | `UrlTree` | The current navigation is cancelled and a new navigation to that tree starts | | `RedirectCommand` | Same as a `UrlTree`, plus `NavigationBehaviorOptions` for the redirect (v18+) | | `Observable` / `Promise` of the above | The router uses the **first** emitted or resolved value, then unsubscribes | Two details matter in interviews. First, when `false` cancels the very first navigation after a page load, nothing is activated in the primary `<router-outlet>`, so the user sees an empty shell. Second, the router does not keep listening to a guard's `Observable`: a later emission does not re-run or reverse the decision. ## Why return a UrlTree instead of navigate() plus false A common older pattern was `router.navigate(['/login']); return false;`. The Angular routing guide explicitly says not to do this. The reasons: - **Two navigations overlap.** Calling `navigate()` inside the guard schedules a second navigation while the first is still running; the router cancels the first as superseded, so the guard's own `false` is moot and the event stream shows a supersession rather than a redirect. - **The router cannot see the intent.** With a `UrlTree` the router records the cancellation as a redirect and hands the original `navigate()` promise over to the redirecting navigation; with `false` plus `navigate()` it sees a rejection followed by an unrelated navigation. - **History handling is lost.** A returned redirect reuses the original navigation's extras, such as `skipLocationChange` and whether to replace the history entry; an imperative call starts from defaults. ## Where this sits among the other guards - `canActivate` decides entry to one route. - `canActivateChild` on a parent decides entry to each of its child routes. - `canMatch` decides whether a route config may match the URL at all; `false` makes the router try the next route. - `canDeactivate` decides whether the user may leave the current route. For an admin area with many child pages, one guard on the `admin` parent (as `canActivate` or `canActivateChild`) avoids repeating it on every child. ## Guards are UX, not security The routing guide opens its guard chapter with a warning: never rely on client-side guards as the sole access control, because all JavaScript in the browser can be modified by the user. The admin guard stops honest users from landing on a page they cannot use; the backend must still reject every admin API call from a non-admin. ## Current recommendation versus older code Many codebases still contain `@Injectable` classes implementing the `CanActivate` interface and listed by class in `canActivate`. Passing a class or `InjectionToken` in a route's guard array has been deprecated since v15.2. New code writes a function; old classes can be adapted with `mapToCanActivate([AdminGuard])` until they are rewritten.

  • What does the user see if an Angular canActivate guard returns false on the very first navigation after a page load?
    The router cancels the navigation and emits `NavigationCancel`, so nothing is activated in the primary `<router-outlet>`: the user sees the app shell with an empty content area. That is why a guard on an entry route should redirect with a `UrlTree` or `RedirectCommand` rather than return `false`.
  • Can an Angular canActivate guard return an HttpClient Observable, and which value does the router use?
    Yes. The return type is `MaybeAsync<GuardResult>`, so an `Observable<boolean | UrlTree>` is fine. The router takes the first emitted value and unsubscribes; later emissions are ignored. Map the response to `true` or a `UrlTree` explicitly, and handle errors, because an error in the guard ends the navigation with a `NavigationError`.

saying these in an interview costs you the question

  • Returning false and calling router.navigate('/login') is the right way to redirect from a guard.
  • Guards must be @Injectable classes that implement the CanActivate interface.
  • A canActivate guard on the admin route is enough to keep admin data secure.
  • The router keeps subscribed to a guard Observable and re-checks on every emission.
  • A guard can only answer synchronously with true or false.
open as a page

In Angular's router, how does a canMatch guard returning false differ from a canActivate guard returning false?

level: middleimportance: must knowfreq 58%

basics

~20 s

canMatch runs while the router matches the URL: false makes it skip that route config and try the next one, possibly a ** fallback. canActivate runs after a route has matched: false cancels the whole navigation.

open as a page

In Angular, how do you build a CanDeactivateFn that warns before leaving a form with unsaved changes, and what does it not catch?

level: middleimportance: must knowfreq 68%

basics

~20 s

A CanDeactivateFn<T> receives the component being left, checks its dirty state and returns true or a confirmation result (boolean, Observable or Promise). It sees only router navigations; reloads, tab closes and external URLs need beforeunload.

open as a page

In Angular, why are class-based route guards deprecated, and how do you migrate one to a functional guard that still uses injected services?

level: middleimportance: should knowfreq 50%

basics

~10 s

Since v15.2, listing an @Injectable class or InjectionToken in a route's guard arrays is deprecated. Rewrite it as a CanActivateFn that calls inject() synchronously, or wrap the class with mapToCanActivate([MyGuard]) while migrating.

open as a page

An Angular route has canActivate: [authGuard, adminGuard]; the admin HTTP check fires even for signed-out users, and some non-admins still get in; what explains both?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Angular invokes every guard in one canActivate array together and uses array order only to rank verdicts, so adminGuard's request always starts. It also ignores results other than boolean, UrlTree or RedirectCommand, so an unmapped response object passes.

open as a page

In Angular, what can a route guard do by returning a RedirectCommand that returning a plain UrlTree cannot?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

A RedirectCommand (v18+) wraps the target UrlTree with NavigationBehaviorOptions that override what the redirect would otherwise inherit from the original navigation, so a guard can set skipLocationChange, replaceUrl, state, info or browserUrl for the redirect.

open as a page