DEV Community

Cover image for Demystifying Angular Route Guards: A Beginner's Guide to Secure Navigation
Pierre Bouillon for This is Angular

Posted on • Updated on

Demystifying Angular Route Guards: A Beginner's Guide to Secure Navigation

When building a web application, chances are that you will have some routing involved at some points.

However, those routes might not be suitable for everyone. As an example, you might not want an anonymous user changing the profile page of someone else.

Angular is no different from the (many) other frameworks and has to solve this issue.

Hopefully, and thanks to its opinionated nature, those controls are baked-in into the framework. However, when getting started with them, it may be a bit obscure at first.

In this article we will see what guards are, how to use them and see some concrete use cases through various scenarios where route guards shine.

So, whether you're interested in restricting access to certain routes, preventing unauthorized actions, or ensuring a smooth user experience, this guide has got you covered. Let's unlock the full potential of Angular route guards and take your application's routing capabilities to new heights!

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 set of functions and classes that control and manage routing and navigation within your Angular application.

They provide a way to protect routes, enforce certain constraints such as authentication or perform other checks on specific routes.

As an example, some use cases that guards can help to manage could be:

  • Preventing an anonymous user to see its profile page
  • Preventing a user without any basket to reach the checkout page
  • Preventing a regular user to access the administration panel

Classes or functions?

Not so long ago, guards where interfaces that needed to be implemented and registered in your modules.

However, since the latest versions of Angular, class-based guards are deprecated in favor of functional guards.

Instead of implementing an interface, it is now possible to use a function to achieve the same result.

Class-based guards can still be used and easily converted into functional ones, thanks to the inject function:

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

There is even a PR that has been submitted to provide helper functions for that purpose.

The different types of route guards

As we previously mentioned it, there is a lot of different use cases for guards such as:

  • Preventing a user to leave a page he has pending edits in
  • Accessing unauthorized views
  • Filtering logged in users

And much more.

Even if those use cases involves routing, the intention is different in each case, that's why the Angular guard's API will also exposes different signatures:

Let's break it down!

CanMatchFn

CanMatchFn is a guard to be used in the context of lazy loading.

When evaluated, it will help Angular's router to determine whether or not the associated lazy-loaded route should be evaluated.

If false, the bundle will not be loaded and the route will be skipped.

Use it when you want to evaluate if the user can load a lazy-loaded route.

🔭 Example
As a regular user, I should not be able to load the /admin route.

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

CanActivateFn

CanActivateFn may be the more intuitive route guard: it helps to determine whether or not the current user can navigate to the route it decorates.

There is no lazy-loading involved here, the route will be effectively loaded and evaluated by the router but its access could be prevented based on the guard's result.

Use it when you want to prevent a user to access a given route.

🔭 Example
As an anonymous user, I should not be able to see my /profile page until I am authenticated.

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

CanActivateChildFn

CanActivateChildFn is the same concept as CanActivateFn but applied to the children of the parent route.

Given a route, evaluating this guard will indicate the router if you can access any of its children.

Use it when you have a parent-children route hierarchy and you want to prevent access to those children but maybe not the parent

🔭 Example
As a coach, I can see my team's details on /team/:id and edit it on /team/:id/edit.
As a regular user, I can only see the team's details on /team/:id but cannot access the edit page on /team/:id/edit.

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

CanDeactivateFn

The CanDeactivateFn is slightly different than the other guards: other guards tend to prevent you from reaching a route, but this one prevent you from leaving it.

There is no concerns about lazy loading here since the route is already loaded and activated.

Use it when you want to prevent a user from losing data potentially tedious to type again or break a multi-steps process.

🔭 Example
As a job candidate, I want to be prompted to confirm before navigating away from the page if my unsaved cover letter is not saved as a draft.

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

Anatomy of a Guard

Guards vary in purpose, but mainly follow the same structure. Given a route, it will either return (synchronously or not):

  • A boolean to indicate whether the route can be reached (true if it can, false otherwise)
  • Or an UrlTree designating a route to redirect the user to

The outputs are the same for all guards, however the input parameters may not. Before implementing the logic bound to a guard, check what its parameters are.

To understand it, let's write a profileGuard which will:

  • Redirect any anonymous user to /login
  • Reject navigation for any user that would access the profile page of someone else
  • Grant navigation for the current user if he wants to browse his own profile page
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

Its usage is no different from our previous example:

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

Combining Route Guards

We covered what guards are and their usages but it is important to note that they are not mutually exclusives.

You are absolutely free to use many of them and even of different sorts.

Consider this example where we would like to grant access to the checkout page only if the user is authenticated and the basket is not empty ; we would also not want our user to accidentally leave the page if the payment is in progress:

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

From a structural point of view, guards also cascade from the parent route to the child route.

It means that guards defined in parents routes will also be evaluated for the child route.

As a result, if you add a guard to ensure that the user is connected on the top of your routes hierarchy, all subsequent routes will be guarded by it.

Inlined Guards

Finally, and thanks to the functional guards, you do not need to create a separate function every time you want to use a guard.

Sometime the logic for a guard is very simple and does not need any additional file or declaration.

As an example, preventing a user to leave the page can be as simple as that:

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

Note that a drawback of this way of writing your guards is that you won't be able to unit test it!

Takeaways

Route guards play a crucial role in controlling and managing access to routes within your application.

In this article, we explored the concept of route guards, their purpose, and how to select the appropriate guard based on specific use cases.

We also learned how to enhance the user experience by leveraging guard hierarchies and implementing the necessary guards. By understanding and utilizing route guards effectively, you can ensure secure and controlled navigation throughout your application.


I hope that you learn something useful there!


Photo by Flash Dantz on Unsplash

Top comments (4)

Collapse
 
nico7522 profile image
Nicolas

Maybe i'm wrong but these code line
"const attemptsToAccessItsOwnPage = currentUser.id !== profilePageId;
return attemptsToAccessItsOwnPage;"
mean it return true if user id is not the same than the profile page ? So the guard let the user pass ?

Collapse
 
pbouillon profile image
Pierre Bouillon

Indeed, thanks for noticing!

Collapse
 
nico7522 profile image
Nicolas

De rien :)

Collapse
 
pbouillon profile image
Pierre Bouillon

Absolutely, this is worth noting!