Angular Lazy Loading and Route Guards: Best Practices and Examples

by Didin J. on Jul 21, 2025 Angular Lazy Loading and Route Guards: Best Practices and Examples

Learn how to implement Angular 20 lazy loading and route guards with standalone APIs for faster, secure apps. Includes role-based auth and real-world examples.

As Angular applications grow in size and complexity, performance and security become crucial. One of the most effective ways to address both concerns is by leveraging lazy loading and route guards in your Angular routing configuration.

Lazy loading enables you to load feature modules only when they’re needed, thereby reducing the initial load time and enhancing application speed. On the other hand, route guards help protect access to specific routes based on conditions such as user authentication or roles, thereby improving the overall security and user experience of your application.

Together, these tools offer a powerful way to build scalable, modular, and secure Angular apps.

In this tutorial, you’ll learn:

  • How lazy loading works in Angular and how to implement it using feature modules.

  • How to use route guards like CanActivate and CanLoad to protect routes.

  • Best practices for organizing routes and securing your application.

  • Real-world examples that combine lazy loading with authentication and role-based access control.

By the end of this tutorial, you’ll be able to optimize your Angular app for performance and security using lazy loading and route guards, ensuring users get the fastest and safest experience possible.


What is Lazy Loading in Angular?

In Angular, lazy loading is a design pattern that delays the loading of modules until they are needed. This improves the initial load time of your application by only loading the essential parts, while feature modules are loaded on demand when the user navigates to a specific route.

Eager Loading vs Lazy Loading

By default, Angular uses eager loading, where all modules are bundled and loaded upfront when the application starts. While this works well for small applications, it can cause performance issues as your app grows.

In contrast, lazy loading loads feature modules dynamically as users navigate through the application. This keeps the initial bundle smaller and speeds up the bootstrapping process.

Loading Strategy Description Pros Cons
Eager Loading Loads all modules at startup Simple to implement Slower initial load time
Lazy Loading Loads modules on demand Faster startup, better performance Requires careful routing setup

Use Cases for Lazy Loading

Lazy loading is ideal for:

  • Admin or dashboard areas are only accessible to certain users

  • Large feature modules like reports, analytics, or settings

  • Multi-role applications with separate modules per user type

How Angular Implements Lazy Loading

Angular implements lazy loading using the loadChildren property in the route configuration. This tells the Angular router to load a module only when its route is activated.

Here’s a basic example:

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

In this example, the AdminModule is not loaded until the user navigates to /admin.


Setting Up an Angular App

To demonstrate lazy loading and route guards, we’ll build a sample Angular application with multiple modules and protected routes.

Prerequisites

Make sure you have the following installed:

  • Node.js (v20 or later)

  • Angular CLI (v20 or later)

Install Angular CLI globally if you haven’t:

npm install -g @angular/cli

Step 1: Create a New Angular Project

ng new angular-lazy-guards --routing --style=css
cd angular-lazy-guards

The --routing flag automatically creates a routing module, which is essential for lazy loading.

Step 2: Generate Feature Routes with Standalone Components

We’ll create two feature sections:

  • admin (protected area)

  • user (public or default area)

Each section will have a dashboard component and its own routing.

ng generate component admin/dashboard --standalone --flat
ng generate component user/dashboard --standalone --flat

Step 3: Manually Create Lazy Route Files

Since the CLI expects a --module (even in standalone mode), we’ll skip the --route flag and instead manually create the routing setup.

Create src/app/admin.routes.ts

import { Routes } from '@angular/router';
import { Dashboard } from './admin/dashboard';

export default [
  {
    path: '',
    component: Dashboard,
  },
] satisfies Routes;

Create src/app/user.routes.ts

import { Routes } from '@angular/router';
import { Dashboard } from './user/dashboard';

export default [
  {
    path: '',
    component: Dashboard,
  },
] satisfies Routes;

Step 4: Configure Main Routing with Lazy Loading

Open src/app/app.routes.ts and update it like this:

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin.routes'),
  },
  {
    path: 'user',
    loadChildren: () => import('./user.routes'),
  },
  {
    path: '',
    redirectTo: 'user',
    pathMatch: 'full',
  },
];

⚠️ You don’t need to use NgModules or the --module flag when manually creating lazy-loaded routes like this — just make sure to return a Routes array from the imported file.


What are Route Guards?

In Angular, route guards are interfaces that allow you to control access to specific routes in your application. They act as gatekeepers, deciding whether a user can navigate to a route, leave a route, or even load a lazy-loaded module.

Guards are especially useful when combined with lazy loading, ensuring that unauthorized users cannot even load certain parts of your application.

Types of Angular Route Guards

Angular provides several built-in guard interfaces:

Guard Interface Description
CanActivate Checks if the route can be activated (navigated to).
CanDeactivate Checks if the user can leave the current route.
CanLoad Prevents the loading of lazy-loaded modules.
CanActivateChild Checks if child routes can be activated.
Resolve Fetches data before the route is activated.

For this tutorial, we’ll focus on the two most important for lazy loading:

  • CanActivate: Controls if a user can access a specific route.

  • CanLoad: Prevents loading of a lazy-loaded route module if the condition is not met.

Why Use Guards?

Use guards to:

  • Protect admin or private routes from unauthorized users.

  • Prevent loading of large feature modules for users without access.

  • Redirect unauthenticated users to a login or error page.

  • Preload data before route activation using Resolve.

Basic Flow

When a user navigates to a guarded route:

  1. Angular calls the guard service (e.g., AuthGuard).

  2. The guard returns:

    • true: allow access.

    • false: deny access.

    • A UrlTree: redirect to another route.

  3. If it's a CanLoad guard, Angular will not even load the route module if access is denied.

In the next section, we’ll build actual route guard services and apply them to the lazy-loaded routes in your Angular 20 standalone app.


Creating and Using Route Guards

Now that you understand how route guards work, let’s build them step by step using Angular 20’s standalone architecture.

We’ll create:

  • An AuthGuard using CanActivate

  • A CanLoadGuard to block lazy-loaded routes

  • A simple AuthService to simulate authentication

Step 1: Create the Auth Service

This service will simulate login and user status.

src/app/auth.service.ts

import { Injectable, computed, signal } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private _isLoggedIn = signal(false); // simulate login status

  readonly isLoggedIn = computed(() => this._isLoggedIn());

  login() {
    this._isLoggedIn.set(true);
  }

  logout() {
    this._isLoggedIn.set(false);
  }
}

Step 2: Create the Auth Guard (CanActivate)

This guard will protect the route after it's loaded.

src/app/auth.guard.ts

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

export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);

  return auth.isLoggedIn() || router.createUrlTree(['/user']);
};

Step 3: Create the CanLoad Guard

This guard will prevent lazy loading of the entire module.

src/app/can-load.guard.ts

import { inject } from '@angular/core';
import { CanLoadFn, Route, UrlSegment, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const canLoadGuard: CanLoadFn = (
  route: Route,
  segments: UrlSegment[]
) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  return auth.isLoggedIn() || router.createUrlTree(['/user']);
};

Step 4: Apply Guards to the Routes

Update main.routes.ts to protect the admin route using both CanLoad and CanActivate.

src/app/app.routes.ts

import { Routes } from '@angular/router';
import { authGuard } from './auth.guard';
import { canLoadGuard } from './can-load.guard';

export const routes: Routes = [
  {
    path: 'admin',
    canLoad: [canLoadGuard],
    canActivate: [authGuard],
    loadChildren: () => import('./admin.routes'),
  },
  {
    path: 'user',
    loadChildren: () => import('./user.routes'),
  },
  {
    path: '',
    redirectTo: 'user',
    pathMatch: 'full',
  },
];

Step 5: Add Simple Login Buttons (Optional for Testing)

In user/dashboard.component.ts, add login/logout buttons to test the guard:

import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-dashboard',
  imports: [],
  templateUrl: './dashboard.html',
  styleUrl: './dashboard.css'
})
export class Dashboard {
  constructor(private auth: AuthService, private router: Router) { }

  login() {
    this.auth.login();
    this.router.navigate(['/admin']);
  }

  logout() {
    this.auth.logout();
    this.router.navigate(['/user']);
  }
}

With this setup:

  • Users must be “logged in” to access the admin route.

  • If not, they’re redirected to /user.

  • The CanLoad guard prevents even loading the admin module.


Best Practices for Angular Lazy Loading and Route Guards

Combining lazy loading with route guards provides both performance optimization and access control. But to use them effectively and securely, consider these best practices:

1. Use CanLoad for Secure Lazy Loading

  • CanLoad ensures a module isn’t even downloaded if access is denied.

  • This is important for protecting sensitive routes like admin dashboards or internal tools.

{
  path: 'admin',
  loadChildren: () => import('./admin.routes'),
  canLoad: [canLoadGuard],
}

⚠️ Without CanLoad, a user could potentially download the code even if they're redirected later.

2. Use CanActivate for In-App Navigation

  • CanActivate checks access after the module is loaded.

  • Ideal for blocking access within already-loaded routes (e.g., when navigating from one internal route to another).

{
  path: 'admin',
  canActivate: [authGuard],
  ...
}

Use both CanLoad and CanActivate together for full protection.

3. Keep Routes Modular and Focused

Structure your app into clear, self-contained feature routes:

  • Example: /admin, /user, /settings

  • Keep feature-specific components in their folders.

This enables better scalability and an easier lazy loading setup.

4. Use Route Data for Role-Based Access

You can pass metadata using the data property to differentiate user roles.

{
  path: 'admin',
  loadChildren: () => import('./admin.routes'),
  canActivate: [roleGuard],
  data: { roles: ['admin'] },
}

Then access it inside your guard:

export const roleGuard: CanActivateFn = (route) => {
  const auth = inject(AuthService);
  const roles = route.data?.['roles'];
  return roles?.includes(auth.getUserRole()) || false;
};

5. Use Preloading Strategically

To improve UX while still using lazy loading, you can enable selective preloading:

RouterModule.forRoot(routes, {
  preloadingStrategy: PreloadAllModules,
});

This preloads lazy modules after initial load in the background.

6. Avoid Guard Logic in Components

Never place guard logic in components (e.g., redirecting inside ngOnInit). Use route guards instead — it centralizes your logic and keeps the component clean.

7. Redirect Unauthorized Access

Always return a UrlTree in your guards to redirect users cleanly.

return router.createUrlTree(['/login']);

This avoids flickering and improper loading behavior.

Following these best practices helps you build fast, secure, and maintainable Angular applications, especially as your project scales.


Real-World Example: Lazy Loading with Auth and Role-Based Guards

In this section, you’ll build a complete flow using:

✅ Lazy-loaded admin and user routes
AuthGuard and CanLoadGuard to protect the admin route
RoleGuard to restrict access based on user roles
✅ Simulated login and logout to test the guards

Step 1: Update the AuthService with Role Logic

Let’s add roles (user and admin) to the service.

src/app/auth.service.ts

import { Injectable, signal, computed } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private _isLoggedIn = signal(false);
  private _role = signal<'user' | 'admin'>('user');

  readonly isLoggedIn = computed(() => this._isLoggedIn());
  readonly role = computed(() => this._role());

  loginAs(role: 'user' | 'admin') {
    this._isLoggedIn.set(true);
    this._role.set(role);
  }

  logout() {
    this._isLoggedIn.set(false);
    this._role.set('user');
  }

  getUserRole(): 'user' | 'admin' {
    return this._role();
  }
}

Step 2: Create a Role-Based Guard

This guard restricts access based on the route’s data.roles array.

src/app/role.guard.ts

import { inject } from '@angular/core';
import { CanActivateFn, ActivatedRouteSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  const allowedRoles: string[] = route.data?.['roles'] || [];

  if (auth.isLoggedIn() && allowedRoles.includes(auth.getUserRole())) {
    return true;
  }

  return router.createUrlTree(['/user']);
};

Step 3: Protect the Admin Route

Update main.routes.ts to include both canLoad and canActivate with role-based protection.

src/app/app.routes.ts

import { Routes } from '@angular/router';
import { canLoadGuard } from './can-load.guard';
import { roleGuard } from './role.guard';

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin.routes'),
    canLoad: [canLoadGuard],
    canActivate: [roleGuard],
    data: { roles: ['admin'] },
  },
  {
    path: 'user',
    loadChildren: () => import('./user.routes'),
  },
  {
    path: '',
    redirectTo: 'user',
    pathMatch: 'full',
  },
];

Step 4: Update the User Dashboard to Simulate Roles

Let users log in as either user or admin.

user/dashboard.ts

import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-dashboard',
  imports: [],
  templateUrl: './dashboard.html',
  styleUrl: './dashboard.css'
})
export class Dashboard {
  constructor(private auth: AuthService, private router: Router) { }

  loginAsUser() {
    this.auth.loginAs('user');
    this.router.navigate(['/user']);
  }

  loginAsAdmin() {
    this.auth.loginAs('admin');
    this.router.navigate(['/admin']);
  }

  logout() {
    this.auth.logout();
    this.router.navigate(['/user']);
  }
}

user/dashboard.html

<h2>User Dashboard</h2>
<p>Log in as:</p>
<button (click)="loginAsUser()">User</button>
<button (click)="loginAsAdmin()">Admin</button>
<button (click)="logout()">Logout</button>

Step 5: Test the Flow

  • Try accessing /admin without logging in → redirected to /user

  • Log in as user/admin still blocked

  • Log in as admin → access granted to /admin

  • Logout → all routes reset to public

✅ You now have a fully working example of Angular 20’s standalone routing, lazy loading, auth/role-based guards, and secure navigation.


Conclusion

In this tutorial, you learned how to use Angular 20’s standalone API to implement lazy loading and route guards, creating a scalable and secure routing structure.

We covered:

  • Setting up a standalone Angular app with lazy-loaded routes

  • Creating and applying CanLoad, CanActivate, and role-based guards

  • Using AuthService to simulate authentication and user roles

  • Best practices for route security, performance, and maintainability

By combining lazy loading with route guards, you ensure that:

  • Only the necessary code is loaded when needed

  • Unauthorized users cannot access or even load restricted modules

  • Your app remains fast, modular, and secure as it grows

These techniques are essential for building enterprise-grade Angular applications that are both user-friendly and well-architected.

You can get the full source code on our GitHub.

If you don’t want to waste your time designing your front-end or your budget to spend by hiring a web designer, then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.

That's just the basics. If you need more deep learning about Angular, you can take the following cheap course:

Thanks!