Skip to the solution
MediumFrontend9 min read

What are functional Route Guards, and how do they differ from class-based guards?

543 views

The problem

Question presented to candidate: "Our codebase has thirty @Injectable guard classes implementing CanActivate. A new teammate writes guards as plain functions. Explain the difference, whether the classes are deprecated in Angular 22, and how you would migrate."

What a strong answer should cover:

  • A class-based guard is an @Injectable class implementing an interface such as CanActivate; the route lists the class as a DI token.
  • A functional guard is a CanActivateFn (or CanMatchFn, CanDeactivateFn ...) that gets dependencies with inject(), because the router calls it inside an injection context.
  • In the 22.2.3 typings, passing a class or token in the route array is deprecated (DeprecatedGuard), while the CanActivate interface itself is not marked deprecated; mapToCanActivate([MyGuard]) wraps a class as a function.
  • Functions are smaller, composable (guard factories like hasRole("admin")), and need no provider registration.
  • inject() must be called synchronously, before any await; afterwards it throws NG0203.
  • Async guards in one array start together, and the result is decided in array order.

Clarifying questions expected:

  • "Are the class guards shared with other code, or only referenced from routes?"
  • "Do the guards have unit tests we need to keep passing during the migration?"

Code / implementation expected: Yes β€” the same rule written as a class, as a mapped class, as a function, and as a guard factory.

  • 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 Β· 11 sections

Read the code

AngularThe same admin rule as a class, a mapped class, a function and a guard factory
import { ApplicationConfig, Component, Injectable, inject, signal } from "@angular/core";
import {
  ActivatedRouteSnapshot, CanActivate, CanActivateFn, GuardResult, MaybeAsync, Router,
  RouterOutlet, mapToCanActivate, provideRouter,
} from "@angular/router";

@Injectable({ providedIn: "root" })
export class AuthService {
  readonly roles = signal<string[]>(["editor"]);
}

// ── Class-based guard (the pre-v14.2 style) ────────────────────────────────
@Injectable({ providedIn: "root" })
export class AdminGuard implements CanActivate {
  private auth = inject(AuthService);
  private router = inject(Router);
  canActivate(): MaybeAsync<GuardResult> {
    console.log("  AdminGuard.canActivate (class)");
    return this.auth.roles().includes("admin") || this.router.parseUrl("/denied");
  }
}

// ── Functional guard: a plain function, inject() works inside it ───────────
export const adminGuard: CanActivateFn = () => {
  console.log("  adminGuard (function)");
  return inject(AuthService).roles().includes("admin") || inject(Router).parseUrl("/denied");
};

// ── Guard factory: configure a function guard per route ────────────────────
export const hasRole = (role: string): CanActivateFn => (route: ActivatedRouteSnapshot) => {
  console.log(`  hasRole("${role}") for /${route.routeConfig?.path}`);
  return inject(AuthService).roles().includes(role) || inject(Router).parseUrl("/denied");
};

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      { path: "legacy", component: PageComponent, canActivate: [AdminGuard] }, // class as a DI token (deprecated form)
      { path: "mapped", component: PageComponent, canActivate: mapToCanActivate([AdminGuard]) }, // class wrapped in a function
      { path: "fn", component: PageComponent, canActivate: [adminGuard] },
      { path: "edit", component: PageComponent, canActivate: [hasRole("editor")] },
      { path: "billing", component: PageComponent, canActivate: [hasRole("billing")] },
      { path: "denied", component: PageComponent },
    ]),
  ],
};

@Component({ selector: "app-root", imports: [RouterOutlet], template: `<router-outlet />` })
export class AppComponent {
  readonly router = inject(Router);
  readonly auth = inject(AuthService);
}

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 49 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

How do you handle errors and retries globally with interceptors?

Continue