Angular

Routing

15 Questions

The RouterModule is configured with route definitions and imported into the root module using forRoot(). You define an array of Routes objects, each mapping a URL path to a component. The wildcard path ** catches any unmatched URLs and is typically used for 404 pages. The <router-outlet> directive in the template marks where routed components are rendered.
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'users', component: UsersComponent },
  { path: '**', component: NotFoundComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

// In the template, use router-outlet for rendering
// <router-outlet></router-outlet>
Use forRoot() in the app module and forChild() in feature modules. The <router-outlet> directive marks where routed components are displayed.

Why it matters: Tests knowledge of routing module setup. Shows foundational routing understanding.

Real applications: Any multi-page Angular application, single-page applications with navigation, dashboard applications, e-commerce sites.

Common mistakes: Developers forget forRoot() vs forChild() distinction. They place router-outlet in wrong location. They don't handle 404 routes.

Route parameters are defined with a colon prefix in the route path like :id. They are accessed via the ActivatedRoute service in the component. You can read them using the snapshot for a one-time read or the paramMap observable for reactive updates. The observable approach is needed when the component stays on screen while the parameter changes.
// Route definition
{ path: 'users/:id', component: UserDetailComponent }

// Accessing params in the component
@Component({ template: '<h2>User {{ userId }}</h2>' })
export class UserDetailComponent implements OnInit {
  userId!: string;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Snapshot (non-reactive)
    this.userId = this.route.snapshot.paramMap.get('id')!;

    // Observable (reactive - responds to param changes)
    this.route.paramMap.subscribe(params => {
      this.userId = params.get('id')!;
    });
  }
}
Use the observable approach when the same component may be reused with different parameters. The snapshot is sufficient when the component is always destroyed and recreated on each navigation.

Why it matters: Tests understanding of route parameter binding. Shows you know when to use reactive patterns.

Real applications: User profile pages with different IDs, product details from route parameters, edit screens with dynamic data.

Common mistakes: Developers use only snapshot and miss parameter changes. They don't unsubscribe from observable, causing memory leaks. They forget non-null assertion (!).

Query parameters are optional key-value pairs appended to the URL after a ? symbol. They are independent of the route path and persist across navigation by default. You can pass them using the [queryParams] binding in templates or the queryParams option in programmatic navigation. The queryParamMap observable on ActivatedRoute is used to read them in the component.
// Navigate with query params
<a [routerLink]="['/products']" [queryParams]="{ sort: 'price', page: 1 }">
  Products
</a>

// Programmatic navigation
this.router.navigate(['/products'], {
  queryParams: { sort: 'price', page: 1 },
  queryParamsHandling: 'merge'  // or 'preserve'
});

// Reading query params
constructor(private route: ActivatedRoute) {}
ngOnInit() {
  this.route.queryParamMap.subscribe(params => {
    const sort = params.get('sort');
    const page = Number(params.get('page'));
  });
}
queryParamsHandling: 'merge' merges new params with existing ones. 'preserve' keeps the current query params unchanged.

Why it matters: Tests knowledge of URL query parameter patterns. Shows you understand filtering and pagination.

Real applications: Pagination (page, size), sorting (sort, order), filtering (category, price range), search results, UI state preservation.

Common mistakes: Developers hardcode parameters in URLs instead of using queryParams. They don't handle queryParamsHandling properly. They don't decode special characters in query strings.

The Router service provides methods for programmatic navigation: navigate() and navigateByUrl(). The navigate() method takes an array of route segments and an optional extras object for query params and fragments. The navigateByUrl() method takes a complete URL string for absolute navigation. Both methods return a Promise<boolean> that resolves to true if navigation succeeds.
import { Router } from '@angular/router';

export class HeaderComponent {
  constructor(private router: Router) {}

  goToUser(id: number) {
    // Navigate with route array
    this.router.navigate(['/users', id]);
  }

  goToSearch(term: string) {
    // Navigate with query params
    this.router.navigate(['/search'], { queryParams: { q: term } });
  }

  goToAbsolute() {
    // Navigate by full URL string
    this.router.navigateByUrl('/about');
  }
}
navigate() accepts an array of route segments and extras. navigateByUrl() accepts a full URL string. Both return a Promise that resolves to true if navigation succeeds.

Why it matters: Tests knowledge of programmatic navigation patterns. Shows you understand imperative routing.

Real applications: Click handlers triggering navigation, conditional routing after form submission, multi-step wizard flows, permission-based redirects.

Common mistakes: Developers don't check if navigation succeeded (the returned Promise). They mix navigate and navigateByUrl inappropriately. They forget relative path navigation.

Lazy loading defers the loading of a feature module until the user navigates to its route. This reduces the initial bundle size and improves startup time significantly. You use the loadChildren property with a dynamic import for modules, or loadComponent for standalone components. Angular automatically creates a separate chunk for the lazy-loaded code during the build.
// App routing module
const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module')
      .then(m => m.AdminModule)
  },
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)  // standalone component
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)]
})
export class AppRoutingModule {}
Use loadChildren for lazy-loading modules and loadComponent for lazy-loading standalone components. The module is fetched only when the user navigates to that route.

Why it matters: Tests knowledge of performance optimization through lazy loading. Shows you understand bundle splitting.

Real applications: Admin panels loaded only for admins, feature modules not used by all users, reducingInitial page load time for large applications.

Common mistakes: Developers don't use lazy loading and ship entire app in one bundle. They mix static and lazy-loaded routes incorrectly. They don't understand that lazy-loaded modules need guards.

Child routes are nested routes rendered inside a parent component's <router-outlet>. They are defined using the children property in the route configuration. Each child route inherits the parent's path prefix, so a child path 'users' under 'admin' becomes /admin/users. The parent component must have its own router-outlet to display the child components.
const routes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      { path: '', component: AdminDashboardComponent },
      { path: 'users', component: AdminUsersComponent },
      { path: 'settings', component: AdminSettingsComponent }
    ]
  }
];

// admin.component.html
// <nav>
//   <a routerLink="users">Users</a>
//   <a routerLink="settings">Settings</a>
// </nav>
// <router-outlet></router-outlet>
The parent component must contain its own <router-outlet> to display child route components. Child routes inherit the parent's path prefix, so 'users' becomes /admin/users.

Why it matters: Tests knowledge of nested routing architectures. Shows you understand layout composition.

Real applications: Admin dashboards with nested modules, multi-level navigation structures, dashboard sections with sub-sections, hierarchical content organization.

Common mistakes: Developers forget the parent component needs its own router-outlet. They don't use relative routing in child routes. They hardcode full paths instead of using relative links.

A wildcard route uses ** as its path to catch any URL that does not match a defined route. It is typically used for displaying a 404 page to the user. The wildcard route must always be placed last in the routes array because Angular matches routes in order. The first matching route wins, so placing it earlier would catch all navigation attempts.
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  // Wildcard route MUST be last
  { path: '**', component: PageNotFoundComponent }
];

@Component({
  selector: 'app-not-found',
  template: '<h1>404 - Page Not Found</h1><a routerLink="/">Go Home</a>'
})
export class PageNotFoundComponent {}
The wildcard route must be the last route in the configuration because Angular matches routes in first-match order. Placing ** earlier would intercept all navigation and prevent other routes from matching.

Why it matters: Tests knowledge of route matching and error handling. Shows you understand pattern matching order.

Real applications: 404 error pages, fallback pages for unhandled routes, catch-all handlers for undefined navigation.

Common mistakes: Developers place ** before specific routes and break navigation. They don't provide a 404 page. They don't handle the wildcard case gracefully.

Route redirects automatically navigate from one route path to another using the redirectTo property.
const routes: Routes = [
  // Redirect empty path to /home
  { path: '', redirectTo: '/home', pathMatch: 'full' },

  // Redirect old URL to new URL
  { path: 'legacy-page', redirectTo: '/new-page', pathMatch: 'full' },

  // Redirect with prefix matching
  { path: 'old', redirectTo: '/new', pathMatch: 'prefix' },

  { path: 'home', component: HomeComponent },
  { path: 'new-page', component: NewPageComponent },
  { path: '**', redirectTo: '/home' }
];
pathMatch: 'full' requires the entire URL to match the path. pathMatch: 'prefix' matches if the URL starts with the path. Always use 'full' for empty path redirects to avoid matching everything.

Why it matters: Tests knowledge of route redirection patterns. Shows you understand URL matching strategies.

Real applications: Redirect empty path to default page, migrate URLs from old structure to new, create vanity URLs, handle backwards compatibility.

Common mistakes: Developers use 'prefix' for empty path redirect, redirecting everything. They don't put redirects before specific routes. They don't understand pathMatch semantics.

ActivatedRoute is a service that provides detailed information about the currently activated route. It exposes paramMap, queryParamMap, data, url, and fragment as observables. You can also access a non-reactive snapshot for one-time reads of route information. It is injected into components to read route parameters, query strings, and static data.
export class ProductComponent implements OnInit {
  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Route parameters
    this.route.paramMap.subscribe(p => console.log(p.get('id')));

    // Query parameters
    this.route.queryParamMap.subscribe(q => console.log(q.get('sort')));

    // Route data (static)
    this.route.data.subscribe(d => console.log(d['title']));

    // URL segments
    this.route.url.subscribe(segments => console.log(segments));

    // Snapshot (non-reactive)
    const id = this.route.snapshot.paramMap.get('id');
  }
}
Key properties: paramMap, queryParamMap, data, url, fragment, outlet, parent, children. Each is available as both an Observable and a snapshot.

Why it matters: Tests knowledge of route metadata access. Shows you understand how to read routing context in components.

Real applications: Read ID from URL to fetch data, get query filters for lists, access breadcrumb titles from route data, detect current route information.

Common mistakes: Developers only use snapshot and miss dynamic updates. They don't unsubscribe from observables, causing memory leaks. They try to access parent/children routes incorrectly.

Angular's Router emits events throughout the navigation lifecycle. You can subscribe to router.events to track navigation progress and show loading indicators. Events include NavigationStart, NavigationEnd, NavigationError, and NavigationCancel. You can filter events by type using the instanceof operator inside the subscription.
import { Router, NavigationStart, NavigationEnd, NavigationError } from '@angular/router';

export class AppComponent {
  loading = false;

  constructor(private router: Router) {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        this.loading = true;
      }
      if (event instanceof NavigationEnd) {
        this.loading = false;
        console.log('Navigated to:', event.urlAfterRedirects);
      }
      if (event instanceof NavigationError) {
        this.loading = false;
        console.error('Navigation failed:', event.error);
      }
    });
  }
}
Key events in order: NavigationStartRouteConfigLoadStart (lazy) → RoutesRecognizedGuardsCheckStartGuardsCheckEndResolveStartResolveEndNavigationEnd.

Why it matters: Tests knowledge of navigation lifecycle and timing. Shows you understand when to apply logic during navigation.

Real applications: Show/hide loading spinner, track analytics events, log navigation, reset UI state, handle errors gracefully.

Common mistakes: Developers don't unsubscribe from router.events, causing memory leaks. They check wrong event types. They don't handle all edge cases (NavigationCancel, NavigationError).

routerLink is a directive that creates navigation links in templates without full page reloads. It takes a URL string or array of route segments and navigates to the specified route when clicked. routerLinkActive is a companion directive that adds a CSS class to the element when the linked route is active. You can configure it with routerLinkActiveOptions for exact URL matching.
<nav>
  <a routerLink="/home" routerLinkActive="active">Home</a>
  <a [routerLink]="['/users', userId]" routerLinkActive="active"
     [routerLinkActiveOptions]="{ exact: true }">Profile</a>
  <a routerLink="/settings" routerLinkActive="active highlight">Settings</a>
</nav>
routerLinkActive can accept multiple CSS class names separated by spaces. Setting exact: true ensures the class is only applied when the full URL matches exactly.

Why it matters: Tests knowledge of template routing directives. Shows you understand declarative navigation.

Real applications: Highlight active menu items, style current navigation links, implement breadcrumb active states, show active tabs.

Common mistakes: Developers forget routerLinkActive completely. They don't use exact matching, causing wrong items to highlight. They pass wrong CSS class names.

forRoot() is used once in the root AppModule to configure the router with routes and register the Router service as a singleton. forChild() is used in feature modules to only register additional routes without re-providing the Router service. If you use forRoot() in a feature module, it creates a second Router instance which breaks navigation. This distinction ensures there is only one Router instance while allowing feature modules to add their own routes.
// App module: forRoot (only once in the entire app)
@NgModule({
  imports: [RouterModule.forRoot(appRoutes)]
})
export class AppModule { }

// Feature module: forChild (can be used in multiple modules)
@NgModule({
  imports: [RouterModule.forChild(featureRoutes)]
})
export class FeatureModule { }
Always use forRoot() only in the AppModule and forChild() in all feature modules. This prevents duplicate Router instances and routing conflicts.

Why it matters: Tests understanding of Angular module architecture. Shows you know singleton vs multi-instance services.

Real applications: Organizing large apps into feature modules, lazy-loaded feature routes, keeping routing centralized yet modular.

Common mistakes: Developers use forRoot() in feature modules, creating multiple Router instances. They don't use forChild() consistently. They don't understand the distinction.

Named router outlets allow you to display multiple routed components simultaneously on the same page. While the primary outlet has no name, secondary outlets are given a name attribute. Each named outlet independently displays a component based on the URL. This is useful for layouts with a sidebar, popup panel, or chat widget alongside the main content.
<!-- Template with multiple outlets -->
<router-outlet></router-outlet>
<router-outlet name="sidebar"></router-outlet>

<!-- Route configuration -->
{ path: 'help', component: HelpComponent, outlet: 'sidebar' }

<!-- Navigation -->
<a [routerLink]="[{ outlets: { sidebar: ['help'] } }]">Show Help</a>
The URL shows named outlets in parentheses like /home(sidebar:help). You can clear a named outlet by navigating to null for that outlet.

Why it matters: Tests knowledge of advanced layout patterns. Shows you understand complex routing scenarios.

Real applications: Sidebars with independent content, popup panels, chat widgets alongside main content, multi-panel layouts.

Common mistakes: Developers don't know about named outlets. They try to force multiple components into a single outlet. They don't understand outlet syntax in URLs.

The data property on a route definition lets you attach static data to a route that can be read by components, guards, or resolvers. This is useful for passing metadata like page titles, breadcrumb labels, required roles, or feature flags. The data is defined once in the route config and stays constant throughout navigation. You access it via the ActivatedRoute service using snapshot or observable.
// Route configuration with static data
{ path: 'admin', component: AdminComponent,
  data: { title: 'Admin Panel', roles: ['admin'], breadcrumb: 'Admin' }
}

// Accessing data in component
export class AdminComponent implements OnInit {
  constructor(private route: ActivatedRoute) {}
  ngOnInit() {
    const title = this.route.snapshot.data['title'];
    const roles = this.route.snapshot.data['roles'];
  }
}
Route data is ideal for static metadata that does not change. For dynamic data fetched from an API, use a resolver instead.

Why it matters: Tests knowledge of route metadata patterns. Shows you can decouple data from code.

Real applications: Setting page titles dynamically, controlling permissions, passing breadcrumb labels, feature toggles, documentation links.

Common mistakes: Developers hardcode metadata in components instead of route data. They don't use route data for permissions. They forget data is type-unsafe without careful validation.

Preloading strategies control how lazy-loaded modules are loaded in the background after the initial app load. Angular provides two built-in strategies: PreloadAllModules loads all lazy modules immediately in the background, and NoPreloading (default) loads modules only when navigated to. You can also create custom preloading strategies that selectively load specific modules based on conditions. Custom strategies use route data flags like preload: true to decide which modules to preload.
// Preload all lazy modules in background
RouterModule.forRoot(routes, {
  preloadingStrategy: PreloadAllModules
})

// Custom preloading: only preload routes with data.preload = true
export class SelectivePreloadStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>) {
    return route.data?.['preload'] ? load() : of(null);
  }
}
PreloadAllModules is a good default for small apps. For larger apps, a custom strategy gives better control over which modules are preloaded based on user behavior.

Why it matters: Tests knowledge of performance optimization with lazy loading. Shows you understand preloading strategies.

Real applications: Improve perceived performance by preloading frequently-used modules, reduce wait time for navigation to common features, optimize user experience.

Common mistakes: Developers use PreloadAllModules on large apps, negating lazy loading benefits. They don't create custom strategies. They preload too aggressively causing unnecessary bandwidth.