Skip to the solution
MediumFrontend9 min read

What are route guards in Angular?

1.0k views

The problem

Question presented to candidate: "We need three rules in our app: only signed-in users can open /account, only admins should even be able to load the admin area, and users with unsaved edits must be warned before they leave the editor. What does Angular give you for this, and in what order do those checks run?"

What a strong answer should cover:

  • A route guard is a function the router calls during navigation; it returns true, false, a UrlTree / RedirectCommand (redirect), or a Promise / Observable of one of those.
  • The guard types and when to use each: canMatch, canActivate, canActivateChild, canDeactivate, plus the deprecated canLoad; resolvers run after all guards.
  • The order: canMatch during matching, then canDeactivate on the route being left, then canActivateChild / canActivate from parent to child, then resolvers.
  • Returning false cancels the navigation (NavigationCancel, navigateByUrl resolves false); returning a UrlTree cancels it and starts a new navigation to that URL.
  • Guards are UX, not security: the server must still check every request.

Clarifying questions expected:

  • "Should users who are not admins see a 404, a 'not allowed' page, or the login page?"
  • "Where does the session state live: a service with a signal, a token in storage, a server call?"

Code / implementation expected: Yes β€” a route config using each guard type, with a log of the order they run in.

  • routing
  • guards

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 9 sections

Read the code

AngularEvery guard type on one route tree, logging the order they run in
import { ApplicationConfig, Component, inject, signal } from "@angular/core";
import {
  CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanMatchFn, NavigationCancel, NavigationEnd,
  ResolveFn, Router, RouterOutlet, provideRouter,
} from "@angular/router";

// A guard is just a function the router calls before it changes the screen.
const step = (msg: string) => console.log(`  ${msg}`);

@Component({ selector: "app-page", template: `<p>{{ name }}</p>` })
export class PageComponent { name = "page"; }

@Component({ selector: "app-editor", template: `<p>Editor (unsaved: {{ dirty() }})</p>` })
export class EditorComponent { dirty = signal(true); }

@Component({ selector: "app-shell", imports: [RouterOutlet], template: `<router-outlet />` })
export class ShellComponent {}

const adminMatch: CanMatchFn = () => { step("canMatch          admin"); return true; };
const adminActivate: CanActivateFn = () => { step("canActivate       admin"); return true; };
const adminChildren: CanActivateChildFn = (child) => { step(`canActivateChild  admin -> ${child.routeConfig?.path}`); return true; };
const usersActivate: CanActivateFn = () => { step("canActivate       admin/users"); return true; };
const adminResolve: ResolveFn<string> = () => { step("resolve           admin"); return "admin data"; };
const usersResolve: ResolveFn<string> = () => { step("resolve           admin/users"); return "users data"; };
const leaveEditor: CanDeactivateFn<EditorComponent> = (c) => { step(`canDeactivate     editor (dirty=${c.dirty()})`); return true; };

// Returning a UrlTree cancels this navigation and starts a new one.
const loggedIn: CanActivateFn = () => {
  step("canActivate       secret -> not logged in, redirect");
  return inject(Router).parseUrl("/login?returnUrl=/secret");
};
const never: CanActivateFn = () => { step("canActivate       blocked -> false"); return false; };

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      { path: "editor", component: EditorComponent, canDeactivate: [leaveEditor] },
      {
        path: "admin", component: ShellComponent,
        canMatch: [adminMatch], canActivate: [adminActivate], canActivateChild: [adminChildren],
        resolve: { info: adminResolve },
        children: [
          { path: "users", component: PageComponent, canActivate: [usersActivate], resolve: { users: usersResolve } },
        ],
      },
      { path: "secret", component: PageComponent, canActivate: [loggedIn] },
      { path: "blocked", component: PageComponent, canActivate: [never] },
      { path: "login", component: PageComponent },
    ]),
  ],
};

@Component({ selector: "app-root", imports: [RouterOutlet], template: `<router-outlet />` })
export class AppComponent {
  readonly router = inject(Router);
  constructor() {
    this.router.events.subscribe((e) => {
      if (e instanceof NavigationCancel) step(`NavigationCancel  ${e.url} (${e.reason || "guard returned false"})`);
      if (e instanceof NavigationEnd) step(`NavigationEnd     ${e.urlAfterRedirects}`);
    });
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 18 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

What is standard content projection vs. Multi-Slot Content Projection?

Continue