Dec 8, 2025

Angular CanMatch Guard: Smart Route Selection Like a Pro

Tags

CanMatch decides whether a route is considered a match, and if it returns false, Angular tries the next matching route instead of blocking navigation outright, which is exactly why it fits A/B testing and alternate experiences so well. Angular also documents that CanMatch is useful for feature flags, A/B testing, and conditional route loading, and that multiple routes can share the same path and let the first matching guard win.


canmatch-guard

How I Used Angular CanMatch to Show Different Dashboards on the Same URL


There was a point while working on my Angular blog platform when I got tired of stuffing version logic inside components. I had one /dashboard URL, but different users were supposed to see different experiences, and I wanted the router to make that decision cleanly for me instead of scattering conditions across templates, services, and component lifecycle hooks. That was the moment CanMatch clicked for me, because it let me keep the same route path while deciding which version should actually match for that specific user.


What I liked most was this: CanMatch did not behave like a “you are blocked” guard in my mind. It behaved more like a smart route selector. If one route for /dashboard didn’t match, Angular simply moved on and checked the next one, which made the whole setup feel natural for user segmentation, experiments, and phased rollouts.


Introduction


When I first started experimenting with different dashboard experiences, I made the classic mistake of trying to solve everything inside one giant component. I had conditions for new users, conditions for returning users, conditions for premium members, and after a while the file started looking like a cupboard where I kept throwing more clothes because I didn’t want to fold anything properly.


At first, I told myself it was fine. “It’s just a few if-else statements,” I thought. But then a new feature request came in. Then another. Then one more. Soon I was not building a dashboard anymore; I was negotiating with a dashboard. Every time I wanted to change something, I had to ask myself, “Will this break the newbie view? What about premium users? Did I just accidentally show the wrong card to the wrong segment?”

That was frustrating.


What I really wanted was simple:

  • Same URL: /dashboard
  • Different users, different dashboard versions
  • Clean code
  • Lazy loading where possible
  • No giant conditional mess inside one component


That is where Angular CanMatch became one of the cleanest routing tools I’ve used. Angular explains that CanMatch runs during route matching, and if it returns false, Angular does not stop everything — it simply tries other route configurations that match the same path. That makes it especially useful for feature flags, experiments, and conditional route selection.

So in this article, I’ll walk through how I think about CanMatch, how I used it in a real-world style scenario, how I’d structure the code more cleanly today, and what small lessons saved me from future routing headaches.


The real problem I was trying to solve


On paper, my requirement sounded tiny: show a different dashboard depending on the user segment.


But in practice, that one sentence hid a lot of mess.


I had users who had just signed up and needed a simpler dashboard with onboarding hints. I had long-time users who were already comfortable and wanted the familiar classic layout. I also had premium users who were paying for extra features, so their dashboard needed deeper analytics and more tools.


Now imagine solving that by loading one component and then branching the UI inside it.


Yes, it works.

But after living through that approach once, I can say from experience that it starts innocent and gets ugly fast. You end up with:


  • Different cards shown conditionally
  • Different API calls for different users
  • Different child components loaded conditionally
  • Different analytics events for each segment
  • A component file that slowly turns into a survival challenge
I didn’t want that anymore.


I wanted the router to decide the experience earlier, before the app got tangled in itself. Angular’s route guard guide specifically notes that CanMatch determines whether a route can be matched during path matching, and that unlike other guards, a failed result falls through so Angular can try another route instead. That one behavior is the reason CanMatch feels so elegant for this use case.


What made CanMatch feel special to me


The best way I can explain CanMatch is this: it helped me move decision-making to the doorway instead of the living room.


With some other approaches, a user enters the route first and then I start deciding what they should see. With CanMatch, I can define multiple routes with the same path and let Angular pick the one that fits the current user. Angular even shows this pattern in its documentation with multiple dashboard routes that resolve to different components depending on which guard matches first.


That was the moment I stopped thinking of CanMatch as just another guard.
I started thinking of it as a route chooser.
That mental shift helped me a lot.


How I structured it in a cleaner way


When I first tried this idea, I made separate guards for each segment. It worked, but later I realised I prefer keeping the segmentation logic centered in one service and making guards very small. That keeps the logic readable and easier to adjust during product changes.


Here’s the kind of structure I’d use.


Step 1: Create a user segment service


This service is where I keep the “who is this user?” logic. I prefer this because business rules change all the time. One week “new user” means 7 days, the next week it becomes 14 days plus onboarding incomplete plus zero published posts. I don’t want that kind of logic hardcoded all over my routing setup.


// user-segment.service.ts
import { Injectable } from '@angular/core';

export type UserSegment = 'newbie' | 'veteran' | 'premium';

@Injectable({ providedIn: 'root' })
export class UserSegmentService {
  getUserSegment(): UserSegment {
    const signupDate = localStorage.getItem('signupDate');
    const plan = localStorage.getItem('subscriptionPlan');
    const hasCompletedOnboarding = localStorage.getItem('onboardingDone') === 'true';

    const days = this.getDaysSinceSignup(signupDate);

    if (plan === 'pro' || plan === 'premium') {
      return 'premium';
    }

    if (days <= 7 || !hasCompletedOnboarding) {
      return 'newbie';
    }

    return 'veteran';
  }

  private getDaysSinceSignup(signupDate: string | null): number {
    if (!signupDate) return 0;

    const createdAt = new Date(signupDate).getTime();
    const now = Date.now();
    const diff = now - createdAt;

    return Math.floor(diff / (1000 * 60 * 60 * 24));
  }
}

This is simple, readable, and very close to how I’d think about the product in real life.


Step 2: Create focused CanMatch guards


Angular supports CanMatch as a route-matching guard, and the API allows the guard to inspect the route and URL segments before Angular decides whether that route should be used. If the guard says no, Angular keeps checking the next matching route instead of treating it like a hard navigation failure.


That is exactly why I like creating small, focused guards.


// newbie-dashboard.guard.ts
import { Injectable } from '@angular/core';
import { CanMatch, Route, UrlSegment } from '@angular/router';
import { UserSegmentService } from './user-segment.service';

@Injectable({ providedIn: 'root' })
export class NewbieDashboardGuard implements CanMatch {
  constructor(private userSegmentService: UserSegmentService) {}

  canMatch(route: Route, segments: UrlSegment[]): boolean {
    return this.userSegmentService.getUserSegment() === 'newbie';
  }
}


// veteran-dashboard.guard.ts
import { Injectable } from '@angular/core';
import { CanMatch, Route, UrlSegment } from '@angular/router';
import { UserSegmentService } from './user-segment.service';

@Injectable({ providedIn: 'root' })
export class VeteranDashboardGuard implements CanMatch {
  constructor(private userSegmentService: UserSegmentService) {}

  canMatch(route: Route, segments: UrlSegment[]): boolean {
    return this.userSegmentService.getUserSegment() === 'veteran';
  }
}


// premium-dashboard.guard.ts
import { Injectable } from '@angular/core';
import { CanMatch, Route, UrlSegment } from '@angular/router';
import { UserSegmentService } from './user-segment.service';

@Injectable({ providedIn: 'root' })
export class PremiumDashboardGuard implements CanMatch {
  constructor(private userSegmentService: UserSegmentService) {}

  canMatch(route: Route, segments: UrlSegment[]): boolean {
    return this.userSegmentService.getUserSegment() === 'premium';
  }
}

I like this setup because each guard reads like a sentence. When I come back after two months, I don’t need to play detective.


Step 3: Let multiple routes compete for the same path


This is where the magic happens.


Angular’s guard documentation explicitly shows that you can define the same path more than once and attach different CanMatch logic to each route. The first route whose guard allows a match becomes the chosen route.


That means I can keep a clean /dashboard URL while serving different experiences.


// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { NotFoundComponent } from './not-found.component';
import { NewbieDashboardGuard } from './guards/newbie-dashboard.guard';
import { VeteranDashboardGuard } from './guards/veteran-dashboard.guard';
import { PremiumDashboardGuard } from './guards/premium-dashboard.guard';

export const routes: Routes = [
  { path: '', component: HomeComponent },

  {
    path: 'dashboard',
    canMatch: [PremiumDashboardGuard],
    loadComponent: () =>
      import('./dashboards/pro-dashboard.component').then(m => m.ProDashboardComponent)
  },
  {
    path: 'dashboard',
    canMatch: [NewbieDashboardGuard],
    loadComponent: () =>
      import('./dashboards/beta-dashboard.component').then(m => m.BetaDashboardComponent)
  },
  {
    path: 'dashboard',
    canMatch: [VeteranDashboardGuard],
    loadComponent: () =>
      import('./dashboards/classic-dashboard.component').then(m => m.ClassicDashboardComponent)
  },

  { path: '**', component: NotFoundComponent }
];

Notice something important here: I put premium first because in many real products, premium access should win before other conditions. Route order matters because Angular evaluates matching routes in the order they are defined. Guards are also executed in the order they appear in a route’s guard array.


That is one of those small details that looks boring until it ruins your day.


The lesson I learned about route order


I once had a situation where a premium user was still seeing the regular dashboard. For ten minutes I blamed everything else: storage values, auth sync, data timing, even my own memory.


The actual problem?


My “veteran” dashboard route was placed before my “premium” dashboard route, and the user qualified for both. So Angular matched the first valid one and moved on. Since CanMatch is fundamentally about route matching, the order of same-path routes becomes part of your business logic whether you like it or not.


That bug taught me something practical: most specific match first, broadest match later.


I still follow that rule.


Why I preferred this over giant component logic


What I loved after switching to CanMatch was how peaceful my components felt.
Instead of one dashboard component trying to be three personalities at once, I had:

  • A beta dashboard focused on onboarding
  • A classic dashboard focused on familiarity
  • A pro dashboard focused on power users
Each component became more honest.

Each one had its own layout, its own copy, its own widgets, and its own analytics. I didn’t have to constantly ask, “Should this card render only for some users?” because that question had already been answered at the route level.

That separation made development easier, testing easier, and future redesigns much easier.

A very practical use case beyond dashboards

The dashboard story is fun, but CanMatch is bigger than that.

After understanding it properly, I started seeing more uses:

  • Feature flags for early rollouts
  • Premium-only route variants
  • Regional versions of the same page
  • A temporary festive landing page for selected users
  • Old vs new UI during migration
  • Admin vs editor experience under the same route path
Angular’s docs explicitly call out feature flags, A/B testing, and conditional route loading as strong use cases for CanMatch, so this isn’t just a creative hack — it’s a pattern the framework itself supports.

One caution I always keep in mind

This part matters.

Angular’s routing guide also warns that client-side guards should never be your only security layer, because any JavaScript running in the browser can be modified by the user. In plain words, CanMatch is fantastic for UX, routing, bundle selection, and experience control, but real authorization must still be enforced on the server side.

I always like saying this clearly because it saves people from a dangerous assumption.

So yes, use CanMatch to decide the route experience.
But no, don’t treat it like your only protection for sensitive data.

My favorite part of this pattern

Honestly, my favorite part was psychological.

Before CanMatch, I used to think:

“Same route means same component, and then I’ll branch inside it.”

After CanMatch, I started thinking:

“Same route can still mean different matched experiences.”

That opened up a cleaner way to design Angular apps in my head.

I stopped pushing routing problems into components and started letting the router do more of the work it was built to do.

That change made my code feel lighter.

Conclusion


If I had to explain CanMatch in one friendly sentence, I’d say this: it helps Angular choose the right route instead of making your component clean up the routing decision later. Angular documents that when CanMatch returns false, the router tries other matching routes rather than simply blocking navigation, which is exactly why it works so nicely for experiments, segmented dashboards, feature flags, and alternate user experiences on the same URL.

For me, this was one of those Angular features that looked small at first but ended up changing how I structure routing problems. The moment I used it for real user segmentation, I stopped seeing it as a fancy extra and started seeing it as one of the cleanest tools for building smarter navigation flows. If you have one URL but multiple valid experiences, CanMatch is probably the elegant solution you were hoping existed.


EmoticonEmoticon