Decoding Route Guards

Most applications reach a point where routing becomes part of the architecture. With routing, however, comes the question of access.

For instance, you probably don't want someone who isn't logged in to modify another user's profile details.

Angular addresses this requirement directly. As a framework with strong opinions, it provides built-in mechanisms to manage these access rules. For newcomers, though, the concept can seem a bit daunting initially.

This guide explores what route guards are, how to implement them, and demonstrates practical scenarios where they prove valuable.

Whether you need to limit access to specific areas, block certain actions, or enhance the overall user journey, this guide explains how Angular route guards can help you achieve these goals. Let's get started.

Table of Contents

  1. Understanding Route Guards
  2. Classes or Functions?
  3. The Different Types of Route Guards
  4. Anatomy of a Guard
  5. Combining Route Guards
  6. Inlined Guards
  7. Takeaways

Understanding Route Guards

Route guards are a collection of functions and classes that oversee routing and navigation within your Angular application.

They offer a mechanism to protect routes, enforce requirements like authentication, or perform checks on specific routes.

For example, guards can help with scenarios such as:

  • Blocking an anonymous user from viewing their profile page
  • Stopping a user with an empty basket from accessing the checkout page
  • Preventing a regular user from entering the administration panel

Classes or functions?

In the past, guards were interfaces that had to be implemented and registered within your modules.

However, with recent versions of Angular, class-based guards have been deprecated in favor of functional guards.

The modern approach involves using a simple function instead of implementing a full interface; the outcome is the same.

Class-based guards remain functional and can be easily transformed into their functional counterparts using the inject function:

const myGuard: CanActivateFn = inject(CanActivateMyGuard).canActivate;

There's also a proposed PR aiming to provide helper functions to simplify this conversion.

The different types of route guards

As mentioned, route guards serve many purposes, including:

  • Preventing a user from navigating away from a page with unsaved changes
  • Restricting access to unauthorized views
  • Managing access for authenticated users

And the list goes on.

Although these scenarios all involve routing, the intent behind each is distinct. This is why Angular's guard API provides several different signatures:

Let's delve into each one!

CanMatchFn

CanMatchFn is designed for the lazy loading context.

When this guard runs, it informs the router whether the associated lazy-loaded route should be considered.

If it returns false, the bundle is never fetched, and the router skips that route entirely.

Use it when you need to check if the user is authorized to load a lazy-loaded module.

🔭 Example
As a standard user, I shouldn't be able to load the /admin route.

const routes: Route[] = [
  { 
    path: 'admin', 
    loadChildren: () => import('./admin').then(m => m.AdminModule),
    canLoad: [AdminGuard] 
  },
];

CanActivateFn

CanActivateFn is perhaps the most straightforward guard: it determines if the current user is allowed to navigate to the route it protects.

In this case, lazy loading isn't a factor. The router loads and evaluates the route, but the guard's result can still block access.

Use it when you want to stop a user from accessing a particular route.

🔭 Example
As an anonymous user, I should not be able to access my /profile page until I log in.

const routes: Route[] = [
  { 
    path: 'profile', 
    component: ProfileComponent,
    canActivate: [authenticationGuard]
  },
];

CanActivateChildFn

CanActivateChildFn applies the same principle as CanActivateFn, but it targets the children of a parent route.

On a given route, this guard tells the router whether access to any of its child routes is permitted.

Use it when you have a nested route structure and want to protect the child routes, potentially while leaving the parent route accessible.

🔭 Example
As a coach, I can view my team's details at /team/:id and modify them at /team/:id/edit.
As a regular user, I can view the team details at /team/:id but am blocked from the edit page at /team/:id/edit.

const routes: Route[] = [
  { 
    path: 'team/:id', 
    component: TeamDetailsComponent,
    canActivateChild: [teamCoachGuard],
    children: [
      { 
        path: 'edit', 
        component: TeamEditComponent
      }
    ]
  },
];

CanDeactivateFn

The CanDeactivateFn differs from the others: instead of preventing access to a route, it prevents the user from leaving the current one.

Lazy loading is irrelevant here because the route is already loaded and active.

Use it when you want to prevent a user from losing data that is difficult to re-enter or from interrupting a multi-step workflow.

🔭 Example
As a job candidate, I should receive a confirmation prompt before navigating away if my unsaved cover letter hasn't been saved as a draft.

const routes: Route[] = [
  { 
    path: 'online-application',
    component: OnlineApplicationComponent,
    canDeactivate: [unsavedChangesGuard]
  },
];

Anatomy of a Guard

Guards have different purposes, but they generally share a common structure. Given a route, a guard returns (either synchronously or asynchronously):

  • A boolean indicating if the route is accessible (true if yes, false if no)
  • Or an UrlTree stating a route to which the user should be redirected

The return type is consistent across all guards, but the input parameters differ. You should review a guard's specific parameters before implementing its logic.

To illustrate, let's build a profileGuard that:

  • Redirects any unauthenticated user to /login
  • Blocks navigation for any user attempting to view someone else's profile page
  • Allows navigation only when the current user is viewing their own profile
const profileGuard: CanActivateFn = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot,
):
  | Observable<boolean | UrlTree>
  | Promise<boolean | UrlTree>
  | boolean
  | UrlTree => {
  const currentUser = inject(CurrentUserService).getCurrentUser();

  // 👇 Redirects to another route
  const isAnonymous = !currentUser;
  if (isAnonymous) {
    return inject(Router).createUrlTree(["/", "login"]);
  }

  const profilePageId = route.params["id"];

  // 👇 Grants or deny access to this route
  const attemptsToAccessItsOwnPage = currentUser.id === profilePageId;
  return attemptsToAccessItsOwnPage;
};
Enter fullscreen mode Exit fullscreen mode

Using it is no different than the earlier examples:

const routes: Route[] = [
  { 
    path: 'profile', 
    component: ProfileComponent,
    canActivate: [profileGuard]
  },
];
Enter fullscreen mode Exit fullscreen mode

Working with Multiple Guards

Guards are not limited to a single use per route — they can be combined freely.

There's no restriction preventing you from applying several guards, even ones of different types, to the same route.

Imagine a scenario where the checkout page should only be reachable for authenticated users with items in their cart, and also shouldn't be left while a payment is processing. That could look like this:

const routes: Route[] = [
  {
    path: 'checkout',
    component: CheckoutComponent,
    canActivate: [authenticationGuard, basketNotEmptyGuard],
    canDeactivate: [paymentInProgressGuard]
  }
];
Enter fullscreen mode Exit fullscreen mode

Guard evaluation follows the route hierarchy — starting from the top-level route and moving down to nested children.

So any guard attached to a parent route is also checked when navigating to a child route under it.

This means that placing an authentication guard at the top of your route tree protects every route beneath it automatically.

Guards Defined Inline

Functional guards open up a simpler possibility: you don't always need to create a dedicated function for guard logic.

For straightforward cases, the guard can be written directly where it's used, without any extra class or file.

For instance, blocking navigation away from a page can be expressed very concisely:

const routes: Route[] = [
  {
    path: 'sign-in',
    component: SignInComponent,
    canDeactivate: [() => !inject(SignInComponent).registrationForm.touched]
  }
];
Enter fullscreen mode Exit fullscreen mode

Keep in mind that guard written this way cannot be unit tested in isolation!

Key Takeaways

Route guards are the primary mechanism for controlling access to your app's routes.

Throughout this guide, we looked at what route guards are, what their purpose is, and how to pick the right one for a given situation.

We also saw how guards can be stacked hierarchically and even declared inline to make navigation both secure and user-friendly. By applying them correctly, you gain fine-grained control over who can navigate where — and when.


Hope you found this useful!


Photo by Flash Dantz on Unsplash