How To Use Angular Route Guards As Feature Toggles

Feature toggles in systems which are heavily user-interface orientated can be a challenge. So far, I haven't found a way to use them effectively without clogging up my code with if statements. But, if you build an Angular app and follow a router-based architecture, then you can make good use of an unconventional feature bundled in Angular: Route Guards.

Why use feature toggles?

Feature toggles allow you to hide certain features of your application, with the aim of not breaking any other features in the process. Two scenarios I often use feature toggles is when I want to enable/disable feature based on the environment (development, testing, live), or if a feature isn't completely ready but the last batch of work needs to be deployed to the live or production environment.

What are Angular Route Guards?

Angular Route Guards were introduced in Angular v2 as part of the new Angular Router. They enable the developer to carry out some logic when a user requests a route, and then allow or deny the user access to that route. A common use is to check if an active user is logged in and authenticated when they try to access the dashboard of an application.

Though Route Guards were never designed to be used for Feature Toggles, the canActivate() method can be used to check a simple boolean feature toggle and determine whether the user of the application can gain access.

You'll need to use environment variables of some sort to make this work. For beginners, the environment.ts file will work fine - and that's what I will use in this tutorial. For more advanced developers, I recommend you check out my post on how to use run-time environment variables.

This tutorial will assume your application is using a router-driven architecture. That means to access the dashboard feature the user must navigate to /dashboard or similar. In the same way to access another feature the user must navigate to /feature or similar. You can determine if you have a router-driven architecture for your Angular application if you have many <router-outlet>s in your template files. Your application is unlikely to be router-driven if you excessively use *ngIf to hide and show features. Chances are, you have a bit of both.

How to use Angular Route Guards as feature toggles

Step 1: Start by adding a feature toggle in your environment.ts file. Something like this will do:

export const environment = {
    production: false,
    ft: {
        enableFeatureA: false
    }
};

Step 2: Next create a FeatureToggleGuardService service which implements CanActivate. You can use the Angular CLI to generate a service like so:

ng generate service services/feature-toggle-guard

And fill your service with the following code:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import environment from '../../environments/environment';

@Injectable()
export class FeatureToggleGuardService implements CanActivate {

    constructor() { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Promise {

        let url = state.url;

        if(url.indexOf('/feature-a/') > -1) {
            return this.allowFeatureA();
        }

        return true;
    }

    allowFeatureA(): boolean {
        if (environment.ft.enableFeatureA == true) {
            return true;
        }

        console.warn("Feature A has been disabled");

        return false;
    }

}

In this service we have created a typical Route Guard which has an implementation of the canActivate() method. This method takes two arguments: an instance of ActivatedRouteSnapshot and an instance of RouterStateSnapshot.

You then use RouterStateSnapshot to determine the URL the user is router to, and use an if statement to check if the URL contains feature a. If it does, you use the method allowFeatureA() to check the environment.ts file to see if the feature toggle has been enabled. If it has been enabled and set to true, return true so the user can navigate to the route. Else, print a warning message to the console and return false.

Don't forget to return true at the end of your canActivate() function so that visiting other routes is not impacted.

Step 3: If you have multiple features you wish to feature toggle, all you need to do is add a check for the URL in the canActivate() method, and use a similar method to check the environment configs. Here's an example:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import environment from '../../environments/environment';

@Injectable()
export class FeatureToggleGuardService implements CanActivate {

    constructor() { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Promise {

        let url = state.url;

        ...
        if(url.indexOf('/feature-b/') > -1) {
            return this.allowFeatureB();
        }
        ...

        return true;
    }

    ...
    allowFeatureB(): boolean {
        if (environment.ft.enableFeatureB == true) {
            return true;
        }

        console.warn("Feature B has been disabled");

        return false;
    }
    ...
}

Step 4: All that is left to do is add the Route Guard to your routes. You can choose whether you add it to all routes, or just the ones that require varying feature toggles. The advantage of adding it to all routes is that you can simplify the pattern used in the development of your application - you will only need to manage Route Guard Feature Toggling in one place. However, bear in mind that the Route Guard will fire every time the user navigates to any route that has a guard associated with it.

Below is how I added the Route Guard to my lazy-loaded routes (based on my multi-module starter on GitHub):

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { FeatureToggleGuardService as FeatureToggleGuard } from './services/feature-toggle-guard.service';

const routes: Routes = [
    {
      path: 'admin/dashboard',
      canActivate: [FeatureToggleGuard, AuthGuardService],
      loadChildren: '../admin-dashboard/admin-dashboard.module#AdminDashboardModule'
    },
    {
      path: 'member/dashboard',
      canActivate: [FeatureToggleGuard],
      loadChildren: '../member-dashboard/member-dashboard.module#MemberDashboardModule'
    },
    {
      path: 'feature-a',
      canActivate: [FeatureToggleGuard],
      loadChildren: '../feature-a/feature-a.module#FeatureAModule'
    },
    {
        path: 'feature-b',
        canActivate: [FeatureToggleGuard],
        loadChildren: '../feature-b/feature-b.module#FeatureBModule'
    }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class RoutingModule { }