DEV Community

Cover image for Angular Routing and Navigation: Complete Guide | Lazy Loading, Guards & Route Protection
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Angular Routing and Navigation: Complete Guide | Lazy Loading, Guards & Route Protection

Building a single-page application sounds simple until you need to handle routing. I've seen Angular applications where every route was eagerly loaded (making the initial bundle huge), routes weren't protected (allowing unauthorized access), and navigation was handled inconsistently. When I learned how to properly configure Angular Router, it transformed how I build applications.

Angular Router is a powerful routing framework that enables navigation between views based on URL changes. It supports lazy loading (loading modules only when needed), route guards (protecting routes based on authentication or authorization), route parameters (dynamic segments like /products/:id), query parameters (for filtering and searching), and nested routes (for complex layouts). Understanding these features is crucial for building scalable Angular applications.

πŸ“– Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.

What is Angular Router?

Angular Router provides:

  • Client-side routing - Navigation without page refreshes
  • Lazy loading - Load modules on-demand for better performance
  • Route guards - Protect routes with authentication/authorization
  • Route parameters - Dynamic route segments
  • Query parameters - Optional filtering and search
  • Nested routes - Complex layouts with child routes
  • Route resolvers - Prefetch data before route activation

Basic Route Configuration

Set up routing in your app routing module:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { BusinessListComponent } from './business/business-list.component';
import { UserListComponent } from './users/user-list.component';

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'business', component: BusinessListComponent },
  { path: 'users', component: UserListComponent },
  { path: '**', redirectTo: '/dashboard' } // Wildcard route (404)
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { 
    enableTracing: false, // Set to true for debugging
    useHash: false // Use hash-based routing if true
  })],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Enter fullscreen mode Exit fullscreen mode

Router Outlet

Add <router-outlet> in your template to display routed components:

<!-- app.component.html -->
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/business">Business</a>
  <a routerLink="/users">Users</a>
</nav>

<router-outlet></router-outlet>
Enter fullscreen mode Exit fullscreen mode

Lazy Loading Modules

Implement lazy loading for better performance:

const routes: Routes = [
  {
    path: 'business',
    loadChildren: () => import('./business/business.module')
      .then(m => m.BusinessModule)
  },
  {
    path: 'users',
    loadChildren: () => import('./users/users.module')
      .then(m => m.UsersModule)
  },
  {
    path: 'sites',
    loadChildren: () => import('./site/site.module')
      .then(m => m.SiteModule)
  }
];
Enter fullscreen mode Exit fullscreen mode

Feature Module Routing

Each lazy-loaded module has its own routing:

// business-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BusinessListComponent } from './business-list.component';
import { BusinessDetailsComponent } from './business-details.component';

const businessRoutes: Routes = [
  { path: '', component: BusinessListComponent },
  { path: ':id', component: BusinessDetailsComponent },
  { path: ':id/settings', component: BusinessSettingsComponent }
];

@NgModule({
  imports: [RouterModule.forChild(businessRoutes)], // Use forChild, not forRoot
  exports: [RouterModule]
})
export class BusinessRoutingModule { }
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Use loadChildren with dynamic import syntax
  • Use RouterModule.forChild() in feature modules
  • Routes are relative to the feature module path
  • Modules load only when their routes are accessed

Route Guards

Protect routes with guards:

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../auth/auth.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated.pipe(
      map(isAuthenticated => {
        if (!isAuthenticated) {
          localStorage.setItem('returnUrl', state.url);
          this.authService.Logout();
          this.router.navigate(['/login']);
          return false;
        }
        return true;
      })
    );
  }
}

// Use in routes
const routes: Routes = [
  {
    path: 'business',
    canActivate: [AuthGuard],
    loadChildren: () => import('./business/business.module').then(m => m.BusinessModule)
  }
];
Enter fullscreen mode Exit fullscreen mode

Multiple Guards

const routes: Routes = [
  {
    path: 'admin',
    canActivate: [AuthGuard, RoleGuard],
    data: { role: 'admin' },
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  }
];
Enter fullscreen mode Exit fullscreen mode

Route Parameters

Access route parameters in components:

// Route definition
{ path: 'business/:id', component: BusinessDetailsComponent }
{ path: 'business/:id/edit/:section', component: BusinessEditComponent }

// Component
import { ActivatedRoute, Router } from '@angular/router';

export class BusinessDetailsComponent implements OnInit {
  businessId: number;

  constructor(
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit(): void {
    // Snapshot (one-time, use when component won't be reused)
    this.businessId = +this.route.snapshot.params['id'];

    // Observable (for dynamic updates when route params change)
    this.route.params.subscribe(params => {
      this.businessId = +params['id'];
      this.loadBusiness();
    });

    // Using paramMap (recommended)
    this.route.paramMap.subscribe(params => {
      this.businessId = +params.get('id')!;
      this.loadBusiness();
    });
  }

  navigateToEdit(): void {
    this.router.navigate(['/business', this.businessId, 'edit']);
  }
}
Enter fullscreen mode Exit fullscreen mode

Multiple Route Parameters

// Route
{ path: 'business/:businessId/site/:siteId', component: SiteDetailsComponent }

// Component
this.route.params.subscribe(params => {
  const businessId = +params['businessId'];
  const siteId = +params['siteId'];
  this.loadSite(businessId, siteId);
});
Enter fullscreen mode Exit fullscreen mode

Query Parameters

Handle query parameters for filtering and searching:

// Navigate with query params
this.router.navigate(['/business'], {
  queryParams: { page: 1, size: 10, search: 'term' }
});

// Navigate with query params and fragment
this.router.navigate(['/business'], {
  queryParams: { filter: 'active' },
  fragment: 'section-1'
});

// Read query params
export class BusinessListComponent implements OnInit {
  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    // Snapshot (one-time)
    const page = this.route.snapshot.queryParams['page'] || 1;
    const size = this.route.snapshot.queryParams['size'] || 10;

    // Observable (for dynamic updates)
    this.route.queryParams.subscribe(params => {
      const page = params['page'] || 1;
      const size = params['size'] || 10;
      const search = params['search'] || '';
      this.loadData(page, size, search);
    });

    // Using queryParamMap (recommended)
    this.route.queryParamMap.subscribe(params => {
      const page = +params.get('page') || 1;
      const size = +params.get('size') || 10;
      this.loadData(page, size);
    });
  }
}

// Preserve query params on navigation
this.router.navigate(['/users'], {
  queryParamsHandling: 'preserve'
});

// Merge query params
this.router.navigate(['/business'], {
  queryParams: { page: 2 },
  queryParamsHandling: 'merge'
});
Enter fullscreen mode Exit fullscreen mode

Template Query Parameters

<!-- Template navigation with query params -->
<a [routerLink]="['/business']" [queryParams]="{page: 1, size: 10}">
  Business List
</a>

<a [routerLink]="['/search']" 
   [queryParams]="{q: searchTerm}" 
   queryParamsHandling="merge">
  Search
</a>
Enter fullscreen mode Exit fullscreen mode

Navigation Methods

Different ways to navigate in Angular:

import { Router, ActivatedRoute } from '@angular/router';

export class MyComponent {
  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) {}

  // Basic navigation
  navigateToBusiness(): void {
    this.router.navigate(['/business']);
  }

  // Navigation with route parameters
  navigateWithParams(): void {
    this.router.navigate(['/business', 123]);
  }

  // Navigation with query parameters
  navigateWithQueryParams(): void {
    this.router.navigate(['/business'], {
      queryParams: { page: 1, filter: 'active' }
    });
  }

  // Relative navigation
  navigateRelative(): void {
    this.router.navigate(['../users'], { relativeTo: this.route });
  }

  // Navigation with options
  navigateWithOptions(): void {
    this.router.navigate(['/business'], {
      queryParams: { page: 1 },
      fragment: 'top',
      queryParamsHandling: 'merge',
      preserveFragment: true
    });
  }

  // Navigate by URL (absolute path)
  navigateByUrl(): void {
    this.router.navigateByUrl('/business/123?page=1');
  }

  // Navigate with state
  navigateWithState(): void {
    this.router.navigate(['/business'], {
      state: { fromDashboard: true }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Template Navigation

<!-- Basic routerLink -->
<a routerLink="/business">Business</a>

<!-- RouterLink with parameters -->
<a [routerLink]="['/business', businessId]">Business Details</a>

<!-- RouterLink with query params -->
<a [routerLink]="['/business']" [queryParams]="{page: 1}">
  Business List
</a>

<!-- Active link styling -->
<a routerLink="/business" routerLinkActive="active">
  Business
</a>

<!-- Active link with exact match -->
<a routerLink="/business" 
   routerLinkActive="active" 
   [routerLinkActiveOptions]="{exact: true}">
  Business
</a>
Enter fullscreen mode Exit fullscreen mode

Nested Routes

Create nested routes for complex layouts:

// Parent route
const routes: Routes = [
  {
    path: 'business',
    component: BusinessLayoutComponent,
    children: [
      { path: '', component: BusinessListComponent },
      { path: ':id', component: BusinessDetailsComponent },
      { path: ':id/settings', component: BusinessSettingsComponent }
    ]
  }
];

// BusinessLayoutComponent template
<div class="business-layout">
  <nav>
    <a routerLink="/business" routerLinkActive="active">List</a>
    <a routerLink="/business/123" routerLinkActive="active">Details</a>
  </nav>
  <router-outlet></router-outlet> <!-- Child routes render here -->
</div>
Enter fullscreen mode Exit fullscreen mode

Route Data

Pass static data to routes:

const routes: Routes = [
  {
    path: 'business',
    component: BusinessComponent,
    data: { 
      title: 'Business Management',
      requiresAuth: true,
      roles: ['admin', 'manager']
    }
  }
];

// Access in component
export class BusinessComponent implements OnInit {
  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    const title = this.route.snapshot.data['title'];
    const requiresAuth = this.route.snapshot.data['requiresAuth'];

    // Or subscribe to data changes
    this.route.data.subscribe(data => {
      console.log(data.title);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Route Resolvers

Prefetch data before route activation:

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { BusinessService } from './business.service';

@Injectable({
  providedIn: 'root'
})
export class BusinessResolver implements Resolve<any> {
  constructor(private businessService: BusinessService) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<any> | Promise<any> | any {
    const businessId = +route.params['id'];
    return this.businessService.GetBusiness(businessId);
  }
}

// Use in routes
const routes: Routes = [
  {
    path: 'business/:id',
    component: BusinessDetailsComponent,
    resolve: { business: BusinessResolver }
  }
];

// Access in component
export class BusinessDetailsComponent implements OnInit {
  business: Business;

  constructor(private route: ActivatedRouteSnapshot) {}

  ngOnInit(): void {
    // Data is already loaded by resolver
    this.business = this.route.snapshot.data['business'];
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Patterns

Route Events

Listen to route changes:

import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

export class AppComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit(): void {
    this.router.events
      .pipe(filter(event => event instanceof NavigationEnd))
      .subscribe((event: NavigationEnd) => {
        console.log('Navigation ended:', event.url);
        // Track analytics, update breadcrumbs, etc.
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

Redirect After Login

export class LoginComponent {
  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) {}

  onLoginSuccess(): void {
    const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
    this.router.navigate([returnUrl]);
  }
}

// In guard
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
  if (!this.authService.isAuthenticated) {
    this.router.navigate(['/login'], {
      queryParams: { returnUrl: state.url }
    });
    return false;
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Use lazy loading for feature modules - Reduce initial bundle size
  2. Implement route guards - For authentication and authorization
  3. Use route data - Pass static data to components
  4. Handle route parameters with observables - For dynamic updates
  5. Use query parameters - For optional, filterable data
  6. Implement proper error handling - For route navigation
  7. Use RouterLink directive - In templates for declarative navigation
  8. Store return URLs - For redirecting after authentication
  9. Use route resolvers - To prefetch data before route activation
  10. Organize routes properly - Use feature routing modules
  11. Handle 404 routes - With wildcard routes
  12. Use route data for permissions - Pass role/permission info

Common Patterns

Route Configuration Structure

// app-routing.module.ts - Main routes
const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  {
    path: 'business',
    canActivate: [AuthGuard],
    loadChildren: () => import('./business/business.module').then(m => m.BusinessModule)
  },
  { path: '**', component: NotFoundComponent }
];
Enter fullscreen mode Exit fullscreen mode

Active Route Styling

<nav>
  <a routerLink="/dashboard" 
     routerLinkActive="active"
     [routerLinkActiveOptions]="{exact: true}">
    Dashboard
  </a>
  <a routerLink="/business" routerLinkActive="active">
    Business
  </a>
</nav>
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

Conclusion

Angular Router provides a comprehensive routing solution for building single-page applications. With lazy loading, route guards, and navigation patterns, you can create scalable, maintainable routing architectures for enterprise Angular applications.

Key Takeaways:

  • Angular Router - Client-side routing framework
  • Lazy Loading - Load modules on-demand for better performance
  • Route Guards - Protect routes with authentication/authorization
  • Route Parameters - Dynamic route segments
  • Query Parameters - Optional filtering and search
  • Nested Routes - Complex layouts with child routes
  • Route Resolvers - Prefetch data before route activation
  • Navigation Methods - RouterLink and programmatic navigation

Whether you're building a simple dashboard or a complex enterprise application, Angular Router provides the foundation you need. It handles all the routing logic while giving you complete control over navigation and route protection.


What's your experience with Angular Routing and Navigation? Share your tips and tricks in the comments below! πŸš€


πŸ’‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.

If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.

Top comments (0)