Angular Routing

Read one Angular interview question at a time, then flip for the complete explanation, example, and code.

0/80 Known filtered set
Level
Read Full Guide Question: 1 of 80

Angular Interview Questions · Angular Routing Companion Article

Angular Routing

Angular applications often behave as Single Page Applications (SPAs). Instead of requesting a completely new HTML page every time the user clicks a link, Angular can change the displayed component while keeping the application loaded in the browser.

The Angular Router is responsible for connecting URLs to components and managing navigation between different parts of an application.

For example:

Text
/                 → Home page
/products         → Product list
/products/101     → Product details
/admin            → Admin dashboard
/login            → Login page

Routing becomes especially important when an Angular application contains multiple pages, feature areas, user roles, or nested screens.

What Is Angular Router?

The Angular Router is the official routing library provided by Angular.

It allows an application to:

  • Display different components for different URLs
  • Navigate without performing a full browser page reload
  • Pass values through URLs
  • Create parent and child routes
  • Protect routes with guards
  • Load features only when required
  • Fetch required data before opening a route
  • Redirect users from one route to another
  • Handle invalid URLs
  • Set browser page titles
  • Observe navigation events

The main router features are available from:

TypeScript
import { Router } from '@angular/router';

A typical Angular routing flow looks like this:

Text
User enters URL
      ↓
Angular Router checks route configuration
      ↓
Matching route is found
      ↓
Guards are evaluated when configured
      ↓
Resolvers obtain required data when configured
      ↓
Component is activated
      ↓
Component appears inside RouterOutlet

Route Configuration

Angular needs to know which component should be displayed for each URL.

Routes are normally defined using the Routes type.

TypeScript
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

export const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'about',
    component: AboutComponent
  }
];

Here:

Text
URL        Component
/          HomeComponent
/about     AboutComponent

The path represents the URL path.

The component specifies which Angular component should be activated when that path matches.

Route Order Matters

Angular processes route definitions in order.

More specific routes should normally appear before broad fallback routes such as:

TypeScript
{
  path: '**',
  component: PageNotFoundComponent
}

The wildcard route should normally appear last because it matches URLs that were not matched by previous routes.

provideRouter()

Standalone Angular applications can configure routing using provideRouter().

Example app.config.ts:

TypeScript
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes)
  ]
};

provideRouter() registers the services required by Angular Router and uses the supplied route configuration.

A common project structure is:

Text
src/
└── app/
    ├── app.component.ts
    ├── app.config.ts
    └── app.routes.ts

Keeping routes in app.routes.ts makes the routing configuration easier to maintain as the application grows.

Router Outlet

Defining routes does not tell Angular where routed components should appear.

That job belongs to RouterOutlet.

HTML
<router-outlet></router-outlet>

For a standalone component, import RouterOutlet:

TypeScript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: `
    <h1>My Application</h1>
    <router-outlet></router-outlet>
  `
})
export class AppComponent {}

Consider these routes:

TypeScript
export const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'products',
    component: ProductsComponent
  }
];

When the URL is:

Text
/products

Angular activates ProductsComponent at the router outlet location.

A typical layout can therefore contain permanent UI around routed content:

HTML
<app-header></app-header>

<main>
  <router-outlet></router-outlet>
</main>

<app-footer></app-footer>

The header and footer remain visible while the routed component changes.

RouterLink

Normal HTML links can use:

HTML
<a href="/products">Products</a>

Inside an Angular SPA, navigation is normally performed with RouterLink.

HTML
<a routerLink="/products">Products</a>

For standalone components:

TypeScript
import { RouterLink } from '@angular/router';

@Component({
  imports: [RouterLink],
  template: `
    <a routerLink="/">Home</a>
    <a routerLink="/products">Products</a>
    <a routerLink="/contact">Contact</a>
  `
})
export class NavbarComponent {}

RouterLink lets Angular Router handle the navigation.

Values can also be supplied dynamically.

HTML
<a [routerLink]="['/products', product.id]">
  View Product
</a>

If:

TypeScript
product.id = 101;

the generated URL becomes:

Text
/products/101

The array syntax is useful when a URL contains dynamic path segments.

RouterLinkActive

RouterLinkActive is commonly used to highlight the currently active navigation link.

HTML
<a
  routerLink="/products"
  routerLinkActive="active">
  Products
</a>

When /products is active, Angular adds the active CSS class.

Example CSS:

CSS
.active {
  font-weight: bold;
  border-bottom: 2px solid currentColor;
}

For links such as the home route, exact matching can be important.

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

Without exact matching, a parent URL may remain considered active while navigating to one of its descendant URLs.

Route Parameters

Route parameters allow values to become part of a URL.

For example:

Text
/products/101
/products/205
/products/900

Here the final segment represents a product ID.

Define the route using ::

TypeScript
{
  path: 'products/:id',
  component: ProductDetailsComponent
}

id is a route parameter.

Reading a Route Parameter

Inject ActivatedRoute:

TypeScript
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-product-details',
  template: `<p>Product ID: {{ productId }}</p>`
})
export class ProductDetailsComponent {

  private route = inject(ActivatedRoute);

  productId = this.route.snapshot.paramMap.get('id');
}

For:

Text
/products/101

the value is:

Text
101

Snapshot vs Observable

A snapshot reads the current route information once.

TypeScript
this.route.snapshot.paramMap.get('id');

When the same component can stay active while parameters change, subscribing to parameter changes can be more appropriate.

TypeScript
this.route.paramMap.subscribe(params => {
  const id = params.get('id');

  console.log(id);
});

For example, navigating directly from:

Text
/products/101

to:

Text
/products/102

may reuse the same component instance, making reactive route information useful.

Multiple Route Parameters

A route can contain multiple parameters.

TypeScript
{
  path: 'users/:userId/orders/:orderId',
  component: OrderComponent
}

Example URL:

Text
/users/25/orders/9001

Read them using:

TypeScript
const userId =
  this.route.snapshot.paramMap.get('userId');

const orderId =
  this.route.snapshot.paramMap.get('orderId');

Route parameters are suitable for values that identify the resource represented by the route.

Query Parameters

Query parameters appear after ? in a URL.

Example:

Text
/products?category=laptop&page=2

Here:

Text
category = laptop
page = 2

Unlike route parameters, query parameters generally represent optional state such as:

  • Search terms
  • Filters
  • Sorting
  • Pagination
  • View mode
  • Tracking information

Create query parameters using RouterLink:

HTML
<a
  routerLink="/products"
  [queryParams]="{
    category: 'laptop',
    page: 2
  }">
  Laptops
</a>

Result:

Text
/products?category=laptop&page=2

Reading Query Parameters

TypeScript
private route = inject(ActivatedRoute);

ngOnInit() {
  this.route.queryParamMap.subscribe(params => {

    const category = params.get('category');
    const page = params.get('page');

    console.log(category);
    console.log(page);

  });
}

Route Parameter vs Query Parameter

Use a route parameter when the value identifies the primary resource:

Text
/products/101

Use query parameters for optional controls:

Text
/products?category=mobile&sort=price

This produces cleaner and more meaningful URLs.

Query Parameter Handling

When navigating, Angular can preserve or merge existing query parameters.

Example:

TypeScript
this.router.navigate(['/products'], {
  queryParams: {
    page: 2
  },
  queryParamsHandling: 'merge'
});

With merge, new query parameters are combined with existing parameters.

Another useful option is:

TypeScript
queryParamsHandling: 'preserve'

This keeps existing query parameters while navigating.

These options can be useful in search, filtering, pagination, and dashboard interfaces.

Fragment Navigation

A URL fragment appears after #.

Example:

Text
/docs#installation

The fragment usually identifies a particular section of a page.

Angular links can specify fragments:

HTML
<a
  routerLink="/docs"
  fragment="installation">
  Installation
</a>

The resulting URL is:

Text
/docs#installation

You can read the current fragment using ActivatedRoute.

TypeScript
this.route.fragment.subscribe(fragment => {
  console.log(fragment);
});

Fragments are useful for:

  • Long documentation pages
  • Table-of-contents navigation
  • FAQ sections
  • Jumping to headings
  • Deep-linking to a specific section

Child Routes

Large applications frequently contain screens that have their own sub-navigation.

For example:

Text
/settings/profile
/settings/security
/settings/notifications

These routes belong logically under /settings.

They can be configured using children.

TypeScript
export const routes: Routes = [
  {
    path: 'settings',
    component: SettingsComponent,
    children: [
      {
        path: 'profile',
        component: ProfileComponent
      },
      {
        path: 'security',
        component: SecurityComponent
      },
      {
        path: 'notifications',
        component: NotificationsComponent
      }
    ]
  }
];

The parent component requires its own router outlet:

HTML
<h2>Settings</h2>

<nav>
  <a routerLink="profile">Profile</a>
  <a routerLink="security">Security</a>
  <a routerLink="notifications">Notifications</a>
</nav>

<router-outlet></router-outlet>

The outer application outlet loads SettingsComponent.

The outlet inside SettingsComponent loads its selected child.

Nested Routes

A routing hierarchy can contain multiple levels.

Example:

Text
/admin
/admin/users
/admin/users/101
/admin/users/101/edit

A possible configuration is:

TypeScript
{
  path: 'admin',
  component: AdminComponent,
  children: [
    {
      path: 'users',
      component: UsersComponent,
      children: [
        {
          path: ':id',
          component: UserDetailsComponent
        }
      ]
    }
  ]
}

Nested routes are particularly useful for applications containing:

  • Admin panels
  • Dashboards
  • Account areas
  • Multi-step workflows
  • Product management systems
  • Documentation sections

Deep nesting should still be used carefully. An unnecessarily complicated route tree can make navigation and maintenance harder.

Default Child Route

A parent route can redirect its empty child path to a default child.

TypeScript
{
  path: 'settings',
  component: SettingsComponent,
  children: [
    {
      path: '',
      redirectTo: 'profile',
      pathMatch: 'full'
    },
    {
      path: 'profile',
      component: ProfileComponent
    },
    {
      path: 'security',
      component: SecurityComponent
    }
  ]
}

Now:

Text
/settings

redirects to:

Text
/settings/profile

Redirect Routes

A redirect automatically sends navigation from one route to another.

TypeScript
{
  path: '',
  redirectTo: 'home',
  pathMatch: 'full'
}

When the user visits:

Text
/

Angular redirects to:

Text
/home

Redirects are useful for:

  • Default pages
  • Old URLs
  • Renamed routes
  • Feature migrations
  • Default child screens

Why pathMatch: 'full' Matters

For an empty-path redirect, pathMatch: 'full' tells the router to apply the redirect only when the complete URL path matches the empty path.

Example:

TypeScript
{
  path: '',
  redirectTo: 'dashboard',
  pathMatch: 'full'
}

This prevents the empty path from unintentionally matching the beginning of other URLs.

Redirecting Old URLs

Suppose an older application used:

Text
/user/25

but the new URL structure is:

Text
/users/25

The route can preserve the parameter:

TypeScript
{
  path: 'user/:id',
  redirectTo: 'users/:id'
}

Existing bookmarks and links can then continue to work.

Wildcard Routes

The wildcard path is:

Text
**

It matches routes that were not matched by earlier definitions.

Example:

TypeScript
{
  path: '**',
  component: PageNotFoundComponent
}

Complete example:

TypeScript
export const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'products',
    component: ProductsComponent
  },
  {
    path: 'contact',
    component: ContactComponent
  },
  {
    path: '**',
    component: PageNotFoundComponent
  }
];

If a user enters:

Text
/something-that-does-not-exist

Angular displays PageNotFoundComponent.

Keep wildcard routes at or near the end of the route configuration because routing uses matching order.

Route Titles

Pages should have meaningful browser titles.

Angular routes can define them directly.

TypeScript
{
  path: 'products',
  title: 'Products',
  component: ProductsComponent
}

Another example:

TypeScript
{
  path: 'contact',
  title: 'Contact Us',
  component: ContactComponent
}

When the route becomes active, the configured title can be used as the document title.

A larger application might use:

TypeScript
export const routes: Routes = [
  {
    path: '',
    title: 'Home',
    component: HomeComponent
  },
  {
    path: 'products',
    title: 'Products',
    component: ProductsComponent
  },
  {
    path: 'about',
    title: 'About Us',
    component: AboutComponent
  }
];

Titles can also be resolved dynamically when the title depends on route-specific data.

For example, a product detail page might ultimately display a title based on the loaded product rather than using one fixed title for every product.

Programmatic Navigation

RouterLink is ideal for template navigation.

Sometimes navigation depends on TypeScript logic.

Examples include:

  • Navigate after login
  • Navigate after saving a form
  • Redirect after deleting an item
  • Move to the next step of a wizard
  • Return to a list after an operation

Inject Router:

TypeScript
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  template: `<button (click)="login()">Login</button>`
})
export class LoginComponent {

  private router = inject(Router);

  login() {

    const loginSuccessful = true;

    if (loginSuccessful) {
      this.router.navigate(['/dashboard']);
    }

  }
}
TypeScript
this.router.navigate([
  '/products',
  productId
]);

For:

TypeScript
productId = 101;

the URL becomes:

Text
/products/101
TypeScript
this.router.navigate(['/products'], {
  queryParams: {
    category: 'laptop',
    page: 1
  }
});

Result:

Text
/products?category=laptop&page=1

Angular also provides navigateByUrl() when you already have the complete destination URL.

TypeScript
this.router.navigateByUrl('/dashboard');

Router Events

Navigation is not one single operation.

The router passes through multiple stages while processing navigation.

Angular exposes these stages through Router.events.

TypeScript
import {
  Router,
  NavigationStart,
  NavigationEnd
} from '@angular/router';

private router = inject(Router);

ngOnInit() {

  this.router.events.subscribe(event => {

    if (event instanceof NavigationStart) {
      console.log('Navigation started');
    }

    if (event instanceof NavigationEnd) {
      console.log('Navigation completed');
    }

  });

}

Important router events include:

EventPurpose
NavigationStartNavigation begins
RoutesRecognizedRouter recognizes matching routes
GuardsCheckStartGuard checking begins
GuardsCheckEndGuard checking completes
ResolveStartResolver phase begins
ResolveEndResolver phase completes
NavigationEndNavigation finishes successfully
NavigationCancelNavigation is cancelled
NavigationErrorNavigation fails
RouteConfigLoadStartLazy route configuration begins loading
RouteConfigLoadEndLazy route configuration finishes loading

Router events are useful for:

  • Loading indicators
  • Analytics
  • Navigation debugging
  • Error tracking
  • Logging
  • Performance measurement

Applications should avoid putting unnecessary heavy work into global router-event subscriptions.

Route Data

Routes can carry static custom data.

Example:

TypeScript
{
  path: 'admin',
  component: AdminComponent,
  data: {
    role: 'admin',
    section: 'management'
  }
}

Read the data through ActivatedRoute:

TypeScript
private route = inject(ActivatedRoute);

ngOnInit() {

  this.route.data.subscribe(data => {
    console.log(data['role']);
    console.log(data['section']);
  });

}

Route data is useful for metadata such as:

  • Permissions
  • Breadcrumb information
  • Layout configuration
  • Feature metadata
  • Page categories
  • UI configuration

Do not use static route data as a replacement for application state that changes frequently.

Route Guards

Route guards control whether navigation should continue.

Typical requirements include:

Text
User opens /dashboard
        ↓
Is the user authenticated?
        ↓
Yes → Continue
No  → Redirect to login

Modern Angular applications can implement guards as functions.

A guard can return values such as:

  • true
  • false
  • A redirect such as a UrlTree or RedirectCommand
  • A Promise
  • An Observable

Guards are useful for client-side navigation control, but they should not be treated as the application's security boundary.

For example, hiding an /admin route in Angular does not replace server-side authorization. Sensitive APIs and data must also be protected by the backend.

CanActivate

CanActivate determines whether a particular route can be activated.

A common use is authentication.

TypeScript
import { inject } from '@angular/core';
import {
  CanActivateFn,
  Router
} from '@angular/router';

export const authGuard: CanActivateFn = () => {

  const router = inject(Router);

  const loggedIn = false;

  if (loggedIn) {
    return true;
  }

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

Apply the guard:

TypeScript
{
  path: 'dashboard',
  component: DashboardComponent,
  canActivate: [authGuard]
}

Now access to /dashboard passes through the guard before route activation.

Multiple Guards

A route can use multiple guards.

TypeScript
{
  path: 'admin',
  component: AdminComponent,
  canActivate: [
    authGuard,
    adminGuard
  ]
}

This can separate different responsibilities such as:

Text
authGuard   → Is the user logged in?
adminGuard  → Does the user have admin permission?

CanActivateChild

CanActivateChild protects child routes under a parent route.

Instead of repeating the same guard:

TypeScript
{
  path: 'admin',
  canActivateChild: [adminGuard],
  children: [
    {
      path: 'users',
      component: UsersComponent
    },
    {
      path: 'reports',
      component: ReportsComponent
    },
    {
      path: 'settings',
      component: AdminSettingsComponent
    }
  ]
}

The guard applies when activating routes inside the protected child hierarchy.

This is useful for areas such as:

Text
/admin/users
/admin/reports
/admin/settings

CanDeactivate

CanDeactivate determines whether the user can leave the current route.

A common scenario is an edit form containing unsaved changes.

Text
User edits form
      ↓
User clicks another page
      ↓
Unsaved changes exist?
      ↓
Yes → Ask before leaving
No  → Continue navigation

Example:

TypeScript
import { CanDeactivateFn } from '@angular/router';

export interface PendingChanges {
  hasUnsavedChanges(): boolean;
}

export const unsavedChangesGuard:
  CanDeactivateFn<PendingChanges> =
  (component) => {

    if (component.hasUnsavedChanges()) {
      return confirm(
        'You have unsaved changes. Leave this page?'
      );
    }

    return true;
  };

Configure it:

TypeScript
{
  path: 'profile/edit',
  component: EditProfileComponent,
  canDeactivate: [unsavedChangesGuard]
}

Typical uses include:

  • Forms
  • Document editors
  • Configuration screens
  • Multi-step data entry
  • Content creation screens

CanMatch

CanMatch determines whether a particular route configuration should be considered a match.

Example:

TypeScript
import { CanMatchFn } from '@angular/router';

export const featureGuard: CanMatchFn = () => {

  const featureEnabled = true;

  return featureEnabled;
};

Apply it:

TypeScript
{
  path: 'new-dashboard',
  component: NewDashboardComponent,
  canMatch: [featureGuard]
}

One useful characteristic of CanMatch is that returning false can allow the router to continue checking other matching route definitions.

For example:

TypeScript
const routes: Routes = [
  {
    path: 'dashboard',
    component: NewDashboardComponent,
    canMatch: [newDashboardGuard]
  },
  {
    path: 'dashboard',
    component: OldDashboardComponent
  }
];

This makes CanMatch useful for scenarios such as:

  • Feature flags
  • Different experiences for different users
  • Conditional route selection
  • Controlled feature rollout

Older Angular learning material may mention CanLoad for some lazy-loading scenarios. Modern routing code should generally prefer CanMatch for route-matching control.

Do Not Perform Manual Navigation Inside Guards

Avoid patterns such as:

TypeScript
if (!loggedIn) {
  router.navigate(['/login']);
  return false;
}

When the purpose is to redirect navigation, return a router redirect representation directly.

For example:

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

This keeps the guard result part of the navigation process itself.

Route Resolvers

Sometimes a component needs important data before it can meaningfully render.

Without a resolver:

Text
Open Product Page
      ↓
Component appears
      ↓
Request product
      ↓
Wait
      ↓
Display product

A resolver can move essential data retrieval into the routing process:

Text
Navigate to Product Page
      ↓
Resolver loads product
      ↓
Navigation continues
      ↓
Component receives route data

Example resolver:

TypeScript
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductService } from './product.service';

export const productResolver: ResolveFn<any> =
  (route) => {

    const productService =
      inject(ProductService);

    const id =
      route.paramMap.get('id');

    return productService.getProduct(id);
  };

Configure it:

TypeScript
{
  path: 'products/:id',
  component: ProductDetailsComponent,
  resolve: {
    product: productResolver
  }
}

Read the resolved data:

TypeScript
private route = inject(ActivatedRoute);

ngOnInit() {

  this.route.data.subscribe(data => {
    console.log(data['product']);
  });

}

Resolvers are particularly useful when the destination route requires essential information before activation.

They should not automatically be used for every request. Non-essential data can often load after the page appears.

Lazy Loading

Large Angular applications can contain many components.

If every feature is included in the initial JavaScript required to start the application, the initial download can become unnecessarily large.

Lazy loading delays loading certain route-related code until it is needed.

Conceptually:

Text
Without Lazy Loading

Application Start
      ↓
Home code
Products code
Admin code
Reports code
Settings code
      ↓
Application ready

With lazy loading:

Text
Application Start
      ↓
Load initial application code
      ↓
Application ready
      ↓
User opens /admin
      ↓
Load admin feature

Lazy loading is especially useful for:

  • Admin areas
  • Large feature sections
  • Reports
  • Settings areas
  • Features rarely visited during the first session
  • Large standalone routed components

Lazy-Loaded Components

Standalone routed components can be lazy-loaded using loadComponent.

TypeScript
export const routes: Routes = [
  {
    path: 'reports',
    loadComponent: () =>
      import('./reports/reports.component')
        .then(m => m.ReportsComponent)
  }
];

The component is loaded when its route is needed instead of being eagerly referenced in the initial route configuration.

A common simple route:

TypeScript
{
  path: 'admin',
  loadComponent: () =>
    import('./admin/admin.component')
      .then(m => m.AdminComponent)
}

This is particularly convenient for standalone Angular applications.

Lazy Loading Child Routes

A feature can also expose its own route configuration.

Main routes:

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

Feature routes:

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

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./admin-home.component')
        .then(m => m.AdminHomeComponent)
  },
  {
    path: 'users',
    loadComponent: () =>
      import('./users.component')
        .then(m => m.UsersComponent)
  }
];

This keeps large feature areas isolated and makes the main route file easier to maintain.

Eager Loading vs Lazy Loading

Eager Loading

The required code is loaded as part of the application's normal initial loading process.

Useful when:

  • The feature is small
  • Users need it immediately
  • It is part of the primary application experience

Lazy Loading

Code is loaded when the associated route or feature becomes necessary.

Useful when:

  • The feature is large
  • It is accessed less frequently
  • Reducing initial JavaScript is important
  • The application has many independent feature areas

Neither technique should be selected blindly. Route design should consider actual application structure and usage.

Preloading

Lazy loading improves initial loading by postponing feature downloads.

However, the first visit to a lazy route may require the browser to fetch additional JavaScript.

Preloading offers a middle ground:

Text
Application starts
      ↓
Initial page becomes usable
      ↓
Angular loads selected lazy routes in background
      ↓
User visits route later
      ↓
Required code may already be available

Angular provides built-in preloading strategies including:

  • NoPreloading
  • PreloadAllModules

NoPreloading leaves lazy routes unloaded until needed.

PreloadAllModules preloads lazy route configurations after initial navigation.

Example:

TypeScript
import {
  provideRouter,
  withPreloading,
  PreloadAllModules
} from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules)
    )
  ]
};

Preloading is useful when an application wants a fast initial load while also reducing delays during later navigation.

Custom Preloading Strategies

Preloading every lazy feature is not always desirable.

Suppose an application contains:

Text
Dashboard
Products
Reports
Admin
Analytics
Settings

Maybe Products and Reports should preload, while Admin and Analytics should wait until requested.

A custom preloading strategy can make this decision.

Routes can carry metadata:

TypeScript
{
  path: 'reports',
  loadChildren: () =>
    import('./reports/reports.routes')
      .then(m => m.REPORT_ROUTES),
  data: {
    preload: true
  }
}

Example strategy:

TypeScript
import { Injectable } from '@angular/core';
import {
  PreloadingStrategy,
  Route
} from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CustomPreloadingStrategy
  implements PreloadingStrategy {

  preload(
    route: Route,
    load: () => Observable<any>
  ): Observable<any> {

    if (route.data?.['preload']) {
      return load();
    }

    return of(null);
  }
}

Configure it:

TypeScript
provideRouter(
  routes,
  withPreloading(CustomPreloadingStrategy)
)

Custom strategies can consider factors such as:

  • Route importance
  • User permissions
  • Feature priority
  • Application state
  • Network conditions
  • Route metadata

Avoid creating complicated preloading logic unless there is a measurable reason for it.

Putting the Routing Features Together

A realistic route configuration might look like this:

TypeScript
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [

  {
    path: '',
    title: 'Home',
    loadComponent: () =>
      import('./home/home.component')
        .then(m => m.HomeComponent)
  },

  {
    path: 'products',
    title: 'Products',
    loadComponent: () =>
      import('./products/products.component')
        .then(m => m.ProductsComponent)
  },

  {
    path: 'products/:id',
    loadComponent: () =>
      import('./product-details/product-details.component')
        .then(m => m.ProductDetailsComponent)
  },

  {
    path: 'dashboard',
    title: 'Dashboard',
    canActivate: [authGuard],
    loadComponent: () =>
      import('./dashboard/dashboard.component')
        .then(m => m.DashboardComponent)
  },

  {
    path: 'admin',
    canActivate: [authGuard],
    loadChildren: () =>
      import('./admin/admin.routes')
        .then(m => m.ADMIN_ROUTES)
  },

  {
    path: 'old-products',
    redirectTo: 'products',
    pathMatch: 'full'
  },

  {
    path: '**',
    loadComponent: () =>
      import('./page-not-found/page-not-found.component')
        .then(m => m.PageNotFoundComponent)
  }

];

This example combines:

  • Static routes
  • Dynamic route parameters
  • Route titles
  • Guards
  • Lazy-loaded components
  • Lazy-loaded route groups
  • Redirects
  • Wildcard handling

Route Parameters, Query Parameters, Data, and Resolvers

These concepts are related but serve different purposes.

FeatureExampleTypical Purpose
Route Parameter/products/101Identify a resource
Query Parameter/products?page=2Optional filters or state
Fragment/docs#routingIdentify a page section
Route Datadata: { role: 'admin' }Static metadata
Resolverresolve: { product: ... }Obtain data before activation

Choosing the correct mechanism keeps routing predictable and URLs meaningful.

RouterLink vs Programmatic Navigation

Use RouterLink when navigation is naturally represented by a link in the template.

HTML
<a routerLink="/products">
  Products
</a>

Use Router.navigate() when navigation depends on application logic.

TypeScript
save() {

  this.productService.save(this.product)
    .subscribe(() => {

      this.router.navigate([
        '/products'
      ]);

    });

}

Using real link semantics for ordinary navigation also keeps templates clearer and is better than turning every navigation action into a click handler.

Common Angular Routing Mistakes

1. Forgetting RouterOutlet

Routes are configured correctly, but no routed component appears.

Check that the appropriate template contains:

HTML
<router-outlet></router-outlet>

2. Putting the Wildcard Route Too Early

Incorrect:

TypeScript
const routes: Routes = [
  {
    path: '**',
    component: PageNotFoundComponent
  },
  {
    path: 'products',
    component: ProductsComponent
  }
];

The wildcard route can capture navigation before later routes are considered.

Place it after normal routes.

3. Forgetting pathMatch on an Empty Redirect

Prefer:

TypeScript
{
  path: '',
  redirectTo: 'home',
  pathMatch: 'full'
}

for a root redirect.

4. Using Query Parameters for Resource Identity

Less clear:

Text
/product?id=101

For a resource-oriented application, this may be clearer:

Text
/products/101

Query parameters are usually better suited to optional controls such as filtering and pagination.

5. Reading Only a Snapshot When Route Values Can Change

If the component remains active while the route parameter changes, relying only on:

TypeScript
this.route.snapshot.paramMap.get('id');

may not represent later parameter changes.

Use reactive route information when the use case requires it.

6. Treating Guards as Backend Security

A route guard runs in client-side application code.

An attacker should never gain access to sensitive information simply because the UI route was hidden or guarded.

Always enforce actual authorization on protected backend resources as well.

7. Lazy Loading Everything

Lazy loading can reduce initial work, but splitting every tiny screen into a separate lazy route can add unnecessary complexity.

Use lazy loading where feature boundaries make sense.

8. Loading Too Much Data in Resolvers

Resolvers can delay route activation.

Only data that genuinely needs to be available before rendering should normally block navigation.

Secondary information can often load after the component appears.

9. Creating Extremely Deep Route Hierarchies

A URL such as:

Text
/admin/company/settings/users/permissions/groups/edit

may indicate that routing and feature boundaries have become unnecessarily complicated.

Keep route structures meaningful and maintainable.

Practical Routing Structure for a Real Application

Consider an e-commerce application.

Possible URLs:

Text
/
/products
/products/101
/products?category=laptop
/cart
/checkout
/account/profile
/account/orders
/admin/products
/admin/orders
/login

A route tree could conceptually be organized as:

Text
Application
│
├── Home
│
├── Products
│   └── Product Details
│
├── Cart
│
├── Checkout
│
├── Account
│   ├── Profile
│   └── Orders
│
├── Admin
│   ├── Products
│   └── Orders
│
├── Login
│
└── Page Not Found

This structure reflects actual application features rather than creating arbitrary URL levels.

Angular Routing Best Practices

Use the following practices when designing routing for larger Angular applications:

  • Keep route definitions organized by feature.
  • Use meaningful URL names.
  • Keep wildcard routes after specific routes.
  • Use route parameters for resource identifiers.
  • Use query parameters for optional filters, sorting, and pagination.
  • Use route titles for meaningful browser page titles.
  • Use RouterLink for declarative navigation.
  • Use Router.navigate() for navigation driven by application logic.
  • Use child routes when a feature has its own nested navigation.
  • Use guards for navigation rules, not as a replacement for server authorization.
  • Prefer functional guard implementations in modern Angular code.
  • Use CanMatch when route matching itself should be conditional.
  • Use resolvers only for data that should be available before activation.
  • Lazy-load meaningful feature boundaries and large routed components.
  • Choose a preloading strategy according to application usage instead of automatically preloading everything.
  • Provide a useful page-not-found experience for unmatched URLs.
  • Keep route configuration readable instead of placing unrelated routing logic in one enormous file.

Angular Routing Mental Model

A useful way to remember routing is:

Text
Routes
   ↓
Describe available URLs

RouterLink / Router.navigate()
   ↓
Request navigation

Router
   ↓
Find matching route

Guards
   ↓
Decide whether navigation is allowed

Resolvers
   ↓
Obtain required route data

RouterOutlet
   ↓
Display activated component

For performance-related routing:

Text
Eager Loading
     ↓
Load immediately

Lazy Loading
     ↓
Load when required

Preloading
     ↓
Load lazy features in background before they are requested

For passing information:

Text
/products/101
      ↓
Route Parameter

/products?page=2
      ↓
Query Parameter

/docs#routing
      ↓
Fragment

data: { role: 'admin' }
      ↓
Static Route Data

resolve: { product: productResolver }
      ↓
Resolved Data

Understanding these relationships is more important than memorizing individual router APIs.

Final Takeaway

Angular Routing is much more than switching between components. It provides the navigation architecture for a complete Angular application.

For small applications, routing may only require a few routes, RouterLink, and RouterOutlet. As the application grows, the same router can handle dynamic parameters, query parameters, nested layouts, route guards, resolvers, lazy loading, page titles, redirects, navigation events, and preloading.

The most maintainable routing configurations reflect the real structure of the application. Keep URLs meaningful, organize routes around features, lazy-load where it provides value, protect sensitive operations on the server as well as in the UI, and use each router feature for the problem it was designed to solve.

Question Hint