Skip to content
Angular Routing Explained — Complete Guide with Guards & Lazy Loading

Angular Routing Explained — Complete Guide with Guards & Lazy Loading

DodaTech Updated Jun 6, 2026 10 min read

Angular routing lets you build single-page applications with multiple views, all without full page reloads. This complete guide explains route definitions, navigation, guards, lazy loading, and more.

What You’ll Learn

  • Configure the Angular Router with route definitions
  • Navigate between views using routerLink and programmatic methods
  • Read route parameters and query parameters
  • Protect routes with guards (auth, unsaved changes)
  • Lazy-load feature modules for better performance
  • Handle 404 pages and redirects

Why Routing Matters

Single-page applications feel like native apps because they swap content without refreshing the page. Every DodaTech product — Doda Browser, DodaZIP, and Durga Antivirus Pro — uses routing to deliver seamless experiences. When you navigate from the Durga dashboard to the scan results page, routing ensures the transition is instant and the URL is bookmarkable. Angular Router makes this trivial to implement.

    flowchart LR
    A["Angular Basics & Forms"] --> B["**Angular Routing**"]
    B --> C["Angular HTTP & Services"]
    style B fill:#f97316,stroke:#c2410c,color:#fff
  
Prerequisites: Angular basics — components, templates, and data binding from the https://tutorials.dodatech.com/frameworks/angular/angular-basics/ tutorial.

How the Angular Router Works

Think of the Router as a map for your app. When a user clicks a link or types a URL, the Router:

  1. Looks at the browser URL
  2. Matches it against your route configuration
  3. Renders the corresponding component in a <router-outlet>
  4. Updates the browser URL without reloading the page
    flowchart TD
    A["Browser URL"] --> B["Router"]
    B --> C["Route Config"]
    C --> D["Router Outlet"]
    D --> E["Matched Component"]
    F["RouterLink"] -->|Click| G["Navigate"]
    G --> B
    D --> H["Nested Routes"]
    H --> I["Child Router Outlet"]
  

Setting Up the Router

If you created your project with --routing, you already have app.routes.ts. If not, here’s how to set it up:

// app.routes.ts
import { Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { AboutComponent } from "./about/about.component";

// Route config: an array of route objects
export const routes: Routes = [
  { path: "", component: HomeComponent, title: "Home" },
  { path: "about", component: AboutComponent, title: "About" }
];
// app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideRouter } from "@angular/router";
import { routes } from "./app.routes";

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)]  // Give the router your config
};

Each route object has:

  • path — The URL segment (empty string "" means the root/home page)
  • component — What to render when this path matches
  • title — Sets the browser tab title automatically

Navigation — Two Ways to Move Between Pages

Template Method: routerLink

<nav>
  <a routerLink="/">Home</a>               <!-- Absolute path -->
  <a routerLink="/about">About</a>
  <a routerLink="/products">Products</a>
  <a [routerLink]="['/products', 42]">Product 42</a>  <!-- With param -->
</nav>

<!-- Where matched components render -->
<router-outlet />
  • routerLink="/" — Navigates to the root. The / makes it absolute (from the root).
  • [routerLink]="['/products', 42]" — Array syntax for routes with parameters. This produces /products/42.
  • <router-outlet /> — This is where the matched component gets rendered. It’s a placeholder.

Active Link Styling

<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
<a routerLink="/about" routerLinkActive="active">About</a>

routerLinkActive="active" adds the CSS class active when the current URL matches the link. For the home route, use { exact: true } so it only highlights when the URL is exactly /, not when it starts with /.

Programmatic Method: Router Service

import { Router } from "@angular/router";

constructor(private router: Router) {}

// Navigate to specific path
this.router.navigate(["/products", id]);

// Navigate relative to current route
this.router.navigate(["edit"], { relativeTo: this.route });

// Replace current history (no back button to this page)
this.router.navigate(["/login"], { replaceUrl: true });

// Navigate by URL string
this.router.navigateByUrl("/about");

Why navigate programmatically? When you need to redirect after a form submission, login, or error — not just from a user clicking a link.

Route Parameters

Route parameters let you create dynamic URLs like /products/42 or /users/alice:

// app.routes.ts
const routes: Routes = [
  {
    path: "products/:id",    // :id is a parameter placeholder
    component: ProductDetailComponent,
    title: "Product Detail"
  }
];
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";

@Component({ ... })
export class ProductDetailComponent implements OnInit {
  productId!: number;

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

  ngOnInit() {
    // Method 1: Snapshot — one-time read (not reactive)
    this.productId = Number(this.route.snapshot.paramMap.get("id"));

    // Method 2: Observable — reacts to param changes (preferred)
    this.route.paramMap.subscribe(params => {
      this.productId = Number(params.get("id"));
      this.loadProduct(this.productId);
    });
  }

  goNext() {
    this.router.navigate(["/products", this.productId + 1]);
  }
}

Why use the Observable approach? If the user navigates from /products/1 to /products/2, Angular reuses the same component instance. The snapshot only has the initial value (1). The Observable emits the new value (2), so your component updates.

Query Parameters

Query parameters are the ?key=value part of a URL. Use them for search queries, pagination, and filters:

// Navigate to /search?q=angular&page=2
this.router.navigate(["/search"], {
  queryParams: { q: "angular", page: 2 }
});

// Read query parameters
this.route.queryParamMap.subscribe(params => {
  const query = params.get("q");
  const page = Number(params.get("page")) || 1;
});
<!-- In template -->
<a [routerLink]="['/search']" [queryParams]="{ q: 'angular', page: 2 }">Search</a>

Nested Routes (Child Routes)

Real apps have layouts with sidebars, tabs, or nested navigation. Nested routes let one component contain its own <router-outlet>:

const routes: Routes = [
  {
    path: "dashboard",
    component: DashboardLayoutComponent,  // Has its own <router-outlet>
    children: [
      { path: "", component: DashboardHomeComponent, title: "Dashboard" },
      { path: "analytics", component: AnalyticsComponent, title: "Analytics" },
      { path: "settings", component: SettingsComponent, title: "Settings" }
    ]
  }
];
<!-- DashboardLayoutComponent template -->
<div class="dashboard-layout">
  <aside>
    <a routerLink="/dashboard">Overview</a>
    <a routerLink="/dashboard/analytics">Analytics</a>
    <a routerLink="/dashboard/settings">Settings</a>
  </aside>
  <main>
    <router-outlet />  <!-- Child components render here -->
  </main>
</div>

Now /dashboard/analytics renders AnalyticsComponent inside DashboardLayoutComponent’s <router-outlet>.

Navigation Guards

Guards are functions that protect routes — they decide whether a user can enter or leave a route:

// auth.guard.ts
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";

export const authGuard: CanActivateFn = (route, state) => {
  const router = inject(Router);
  const isAuthenticated = localStorage.getItem("token");

  if (!isAuthenticated) {
    // Redirect to login with return URL
    return router.parseUrl("/login?redirect=" + state.url);
  }
  return true;  // Allow access
};
// Apply to routes
const routes: Routes = [
  {
    path: "admin",
    component: AdminComponent,
    canActivate: [authGuard],   // Guard runs before route activates
    title: "Admin"
  }
];

The guard returns:

  • true — Allow navigation
  • false — Block navigation (shows nothing)
  • UrlTree — Redirect to a different URL (preferred)

Guard Types

GuardWhen It RunsUse Case
canActivateBefore entering a routeAuth check
canActivateChildBefore entering any child routePermission check
canDeactivateBefore leaving a routeUnsaved changes warning
canMatchBefore matching a routeFeature flags
resolveBefore activating, prefetch dataLoad data before showing page

Lazy Loading — Faster Initial Loads

Lazy loading means splitting your app into chunks that load only when needed. Your initial bundle stays small, and users don’t download code they haven’t visited yet:

const routes: Routes = [
  {
    path: "admin",
    loadComponent: () => import("./admin/admin.component").then(m => m.AdminComponent),
    canActivate: [authGuard],
    title: "Admin"
  },
  {
    path: "products",
    loadChildren: () => import("./products/products.routes").then(m => m.productRoutes)
  }
];
// products/products.routes.ts
import { Routes } from "@angular/router";

export const productRoutes: Routes = [
  { path: "", component: ProductListComponent, title: "Products" },
  { path: ":id", component: ProductDetailComponent, title: "Product Detail" }
];

loadComponent loads a single component lazily. loadChildren loads an entire set of child routes. Both use dynamic import() syntax — a native JavaScript feature that returns a Promise.

Redirects & 404 Handling

const routes: Routes = [
  { path: "", component: HomeComponent },
  { path: "old-path", redirectTo: "/new-path", pathMatch: "full" },
  { path: "**", component: NotFoundComponent, title: "Page Not Found" }
];
  • redirectTo — Redirects from one path to another automatically.
  • pathMatch: "full" — Only redirect if the entire path matches (not just a prefix).
  • path: "**" — The wildcard route catches any path that didn’t match anything above. Always put this last.

Scroll Behavior & Title Strategy

Angular Router can automatically manage scrolling and document titles:

// app.config.ts
import { provideRouter, withRouterConfig } from "@angular/router";

providers: [
  provideRouter(routes,
    withRouterConfig({
      scrollPositionRestoration: "top",   // Scroll to top on navigation
      anchorScrolling: "enabled"          // Enable #fragment scrolling
    })
  )
]

Setting title on each route (title: "Home") automatically updates the browser tab title when navigating.

Common Mistakes

1. Not using the wildcard route for 404 pages

Without { path: '**', component: NotFoundComponent }, unknown paths show a blank page. Place the wildcard route last since Angular evaluates routes in order.

2. Using snapshot.paramMap when parameters can change

snapshot.paramMap.get("id") only reads the initial value. If the user navigates from /products/1 to /products/2, the component reuses and snapshot doesn’t update. Use paramMap.subscribe() instead.

3. Missing relativeTo for relative navigation

this.router.navigate(['edit']) navigates from the root (/edit), not from the current route. Add { relativeTo: this.route } to navigate relative to the active route.

4. Not implementing canDeactivate for forms

Users may accidentally leave a form with unsaved changes. Use canDeactivate guard to show a confirmation dialog.

5. Overusing eager loading for large feature modules

If every route uses eager loading, your initial bundle grows unnecessarily. Lazy load feature routes with loadComponent or loadChildren for better performance.

6. Incorrect routerLinkActive with nested routes

Without { exact: true }, the home link (/) stays highlighted on every page because all paths start with /. Always use exact matching for the root route.

Practice Questions

  1. What is the difference between snapshot.paramMap and paramMap Observable? snapshot.paramMap gives a one-time value that doesn’t update when parameters change. paramMap is an Observable that emits new values whenever parameters change.

  2. How do you protect a route from unauthorized users? Use canActivate guard. The guard function checks for authentication (e.g., a token in localStorage) and returns true, false, or a UrlTree for redirect.

  3. What does loadComponent do? It lazy-loads a component — the component’s code is split into a separate JavaScript chunk that downloads only when the user visits that route.

  4. How do you create nested routes? Add a children array to a parent route. The parent component needs its own <router-outlet> where child components render.

  5. What is the purpose of pathMatch: 'full'? It ensures the redirect only happens when the URL matches the entire path, not just a prefix. Without it, /old-path/extra would also match.

Challenge

Build a product catalog with: a home page, a lazy-loaded products list, a product detail page with route parameter :id, an auth guard on the checkout route, and a 404 page. Add active link styling to the navigation.

FAQ

How do I navigate programmatically?
Inject Router and call router.navigate([’/path’, param]) or router.navigateByUrl(’/path’).
What is the difference between ActivatedRoute snapshot and paramMap?
snapshot gives the initial value and doesn’t update when params change. paramMap is an Observable that emits new values on param changes.
How do I protect routes?
Use canActivate guard with a function that returns true (allow), false (block), or a UrlTree (redirect).
What is lazy loading?
Deferring the loading of route components until they’re visited. Use loadComponent for standalone components or loadChildren for feature modules.
How do I handle 404s?
Add a wildcard route { path: ‘**’, component: NotFoundComponent } as the last route in the configuration.

Try It Yourself

Create a new Angular project with routing and implement:

  1. Three pages: Home, About, and Products (lazy-loaded)
  2. Navigation bar with routerLink and routerLinkActive
  3. Product detail page that reads an :id parameter
  4. An auth guard that redirects to /login if no token exists
  5. A <router-outlet> where pages render
// app.routes.ts — starter structure
import { Routes } from "@angular/router";
import { authGuard } from "./guards/auth.guard";

export const routes: Routes = [
  { path: "", loadComponent: () => import("./home/home.component").then(m => m.HomeComponent), title: "Home" },
  { path: "about", loadComponent: () => import("./about/about.component").then(m => m.AboutComponent), title: "About" },
  {
    path: "products",
    loadChildren: () => import("./products/products.routes").then(m => m.productRoutes),
    title: "Products"
  },
  { path: "admin", loadComponent: () => import("./admin/admin.component").then(m => m.AdminComponent), canActivate: [authGuard] },
  { path: "**", loadComponent: () => import("./not-found/not-found.component").then(m => m.NotFoundComponent) }
];

What’s Next

TutorialDescription
https://tutorials.dodatech.com/frameworks/angular/angular-http/Connect to backends with HTTP Client and services
https://tutorials.dodatech.com/frameworks/angular/angular-advanced/Signals, performance optimization, dynamic components
https://tutorials.dodatech.com/frameworks/angular/cli/Angular CLI commands for scaffolding and building

Related topics: TypeScript, REST API, JavaScript/ES Modules.

What’s Next

Congratulations on completing this Angular Routing tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro