Tutorials Logic, IN info@tutorialslogic.com

Angular Route Guards CanActivate Auth

Guard Boundary

Route guards participate in navigation decisions; they do not secure a server API. Learn functional guards, UrlTree redirects, unsaved-change checks, resolvers, and the difference between authorization and code-loading boundaries.

A route guard participates in client navigation and returns boolean, UrlTree, RedirectCommand, Promise, or Observable. Functional guards can inject services directly and are the clearest default for new routes.

Use canActivate for entry policy, canActivateChild for a child tree, canMatch to choose whether a route configuration matches, and canDeactivate to ask whether the current component may be left. Use a resolver for required route data.

A guard is not security enforcement because it runs in user-controlled browser code. The server must authenticate and authorize every protected operation.

Guard Purpose
canActivate Decides if a route can be activated (navigated to)
canActivateChild Decides if child routes can be activated
canDeactivate Decides if a route can be left (e.g. unsaved changes)
canMatch Decides if a route definition should be matched at all
resolve Pre-fetches data before the route activates

Activation Guards

Use canActivate when entry to a matched route depends on application state such as a signed-in session or completed onboarding. Return a redirect result instead of returning false and starting a second navigation imperatively.

canActivate - Auth Guard

canActivate - Auth Guard
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

// Functional guard with dependencies resolved from the route injection context
export const authGuard: CanActivateFn = (route, state) => {
    const auth   = inject(AuthService);
    const router = inject(Router);

    if (auth.isLoggedIn()) {
        return true;
    }

    // Redirect to login, preserving the intended URL
    return router.createUrlTree(['/login'], {
        queryParams: { returnUrl: state.url }
    });
};

// auth.service.ts (simplified)
// @Injectable({ providedIn: 'root' })
// export class AuthService {
//     private loggedIn = signal(false);
//     isLoggedIn() { return this.loggedIn(); }
//     login()  { this.loggedIn.set(true); }
//     logout() { this.loggedIn.set(false); }
// }

canActivate - Protecting Routes

canActivate - Protecting Routes
import { Routes } from '@angular/router';
import { authGuard } from './auth.guard';
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './login/login.component';

export const routes: Routes = [
    { path: 'login', component: LoginComponent },
    {
        path: 'dashboard',
        component: DashboardComponent,
        canActivate: [authGuard]   // protect this route
    },
    {
        path: 'admin',
        loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
        canActivate: [authGuard]   // protect entire lazy-loaded section
    }
];

Route Matching with canMatch

Use canMatch when a condition should decide whether the router selects a route definition at all. Angular evaluates it during route recognition, before activation. A true result keeps the route as a candidate; false makes the router continue checking later route definitions that match the same URL.

A CanMatchFn receives the Route configuration, the remaining UrlSegment array, and the PartialMatchRouteSnapshot built so far. It can return boolean, UrlTree, RedirectCommand, Promise, or Observable. Return a UrlTree or RedirectCommand when the user must be redirected; returning false means "try another route", not "go to login".

canMatch is useful for feature flags, staged rollouts, role-specific screens at one URL, and lazy routes that should not be selected for the current user. It replaces the deprecated canLoad guard. It can stop lazy code from loading as part of that navigation, but it is still browser-side routing logic and never replaces server authorization.

  • Place a guarded route before its same-path fallback because Angular evaluates route definitions in order.
  • If canMatch returns false and no later route matches, normal wildcard or not-found routing applies.
  • Use canActivate after matching when the route is correct but activation still requires an entry check.
  • Configured preloading can fetch lazy code independently, so do not treat canMatch as a confidentiality boundary.

canMatch - Feature Flag Guard

canMatch - Feature Flag Guard
import { inject } from '@angular/core';
import { CanMatchFn } from '@angular/router';
import { FeatureFlagsService } from './feature-flags.service';

export const featureFlagGuard: CanMatchFn = (
    route,
    _segments,
    _currentSnapshot
) => {
    const flags = inject(FeatureFlagsService);
    const feature = route.data?.['feature'] as string;

    // false tells the router to try the next matching route.
    return flags.isEnabled(feature);
};

canMatch - Lazy Route with a Fallback

canMatch - Lazy Route with a Fallback
import { Routes } from '@angular/router';
import { featureFlagGuard } from './feature-flag.guard';

export const routes: Routes = [
    {
        path: 'dashboard',
        data: { feature: 'new-dashboard' },
        canMatch: [featureFlagGuard],
        loadComponent: () =>
            import('./new-dashboard.component')
                .then(m => m.NewDashboardComponent)
    },
    {
        // Used when featureFlagGuard returns false.
        path: 'dashboard',
        loadComponent: () =>
            import('./classic-dashboard.component')
                .then(m => m.ClassicDashboardComponent)
    }
];

Unsaved-change Guards

Use canDeactivate to warn before Angular navigation leaves a form with unsaved changes. Keep the component contract small, such as hasUnsavedChanges(), so the guard does not depend on one form implementation.

Only prompt when saved data differs from the current form, and skip the prompt after a successful save. Browser beforeunload is a separate boundary for closing or refreshing the tab; a router guard only sees Angular navigation.

canDeactivate - Unsaved Changes Guard

canDeactivate - Unsaved Changes Guard
import { CanDeactivateFn } from '@angular/router';

// Define an interface for components that can be guarded
export interface CanComponentDeactivate {
    canDeactivate: () => boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
    if (component.canDeactivate()) {
        return true;
    }
    return confirm('You have unsaved changes. Leave anyway?');
};

canDeactivate - Preventing Unsaved Changes

canDeactivate - Preventing Unsaved Changes
import { Component, signal } from '@angular/core';
import { CanComponentDeactivate } from './unsaved-changes.guard';

@Component({
    selector: 'app-edit-form',
    standalone: true,
    template: `
        <input [(ngModel)]="name" (ngModelChange)="isDirty.set(true)" />
        <button (click)="save()">Save</button>
        <p *ngIf="isDirty()">You have unsaved changes</p>
    `
})
export class EditFormComponent implements CanComponentDeactivate {
    name   = '';
    isDirty = signal(false);

    save() {
        // ... save logic
        this.isDirty.set(false);
    }

    canDeactivate(): boolean {
        return !this.isDirty();
    }
}

Route Resolvers

A resolver loads required route data before activation. The component can read the resolved value from route data or a configured component input, while optional or progressively loaded data can remain a component-owned loading state.

resolve - Data Pre-fetching

resolve - Data Pre-fetching
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { HttpClient } from '@angular/common/http';

export interface User { id: number; name: string; email: string; }

export const userResolver: ResolveFn<User> = (route) => {
    const http = inject(HttpClient);
    const id   = route.paramMap.get('id');
    return http.get<User>(`https://api.example.com/users/${id}`);
};

resolve - Pre-fetching Data

resolve - Pre-fetching Data
import { Routes } from '@angular/router';
import { userResolver } from './user.resolver';
import { UserDetailComponent } from './user-detail.component';

export const routes: Routes = [
    {
        path: 'users/:id',
        component: UserDetailComponent,
        resolve: { user: userResolver }
    }
];

// In UserDetailComponent:
// constructor(private route: ActivatedRoute) {}
// ngOnInit() {
//     const user = this.route.snapshot.data['user'];
// }
Confirm the page outcome

Guard Review

5 checks
  • Treat a guard as a client-side navigation decision, never as server authorization.
  • Return a UrlTree when access should continue at another route instead of calling navigate as a side effect.
  • Reset the saved or dirty state that drives canDeactivate only after persistence succeeds.
  • Define the user experience for resolver failure, cancellation, and slow data.
  • Use canMatch when access rules should also control whether a lazy route is selected and loaded.

Guard Review Questions

It expresses the redirect as the guard’s result. The router cancels the original navigation and starts the replacement cleanly.

canActivate decides whether the route may activate after route matching and loading have progressed. Use canMatch when unauthorized users should not match that lazy route in the first place; this can prevent the router from selecting and loading that route configuration.

The component’s dirty flag or form state was not reset after a successful save. Set the form pristine or update the signal only after the server confirms persistence; clearing it before the request succeeds can lose the warning when saving fails.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.