B · Structure → Module 06
Module 06 · Questions 28–32

Routing

Routes, params, guards and lazy loading. Q29 contains the bug everyone hits once.

Q28

How does routing work?

const routes: Routes = [
  { path: '',            component: HomeComponent },
  { path: 'orders',      component: OrderListComponent },
  { path: 'orders/:id',  component: OrderDetailComponent },
  { path: 'login',       component: LoginComponent },
  { path: '**',          component: NotFoundComponent }   // wildcard LAST
];

@NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] })
export class AppRoutingModule { }
<nav>
  <a routerLink="/orders" routerLinkActive="active">Orders</a>
</nav>

<router-outlet></router-outlet>   <!-- the matched component renders here -->
forRootOnce, in the app module
forChildIn every feature module
routerLinkNot href — href reloads the whole app
**Wildcard for 404. Must be the last route — matching is first-wins

Navigating in code: this.router.navigate(['/orders', id]).

Q29

Reading route parameters

⚠ The bug everyone hits

You're on /orders/1 and navigate to /orders/2. Angular reuses the same component instance — it doesn't destroy and recreate it. So ngOnInit never runs again, and the screen still shows order 1.

// ❌ only works the first time
ngOnInit() {
  const id = this.route.snapshot.paramMap.get('id');
  this.load(id);
}

// ✅ re-runs whenever the id changes
ngOnInit() {
  this.route.paramMap
      .pipe(takeUntil(this.destroy$))
      .subscribe(p => this.load(p.get('id')!));
}

// ✅ even better — switchMap cancels the previous request
orders$ = this.route.paramMap.pipe(
  switchMap(p => this.api.get(Number(p.get('id'))))
);
paramMapPath params — /orders/:id
queryParamMapQuery string — ?page=2&sort=date
snapshotA one-off read. Only safe if the component is always recreated

That last version pairs nicely with the async pipe — no manual subscribe, nothing to unsubscribe, and the in-flight request is cancelled when the user clicks the next order.

Q30Child routes
{
  path: 'orders/:id',
  component: OrderDetailComponent,      // has its own <router-outlet>
  children: [
    { path: '',        redirectTo: 'summary', pathMatch: 'full' },
    { path: 'summary', component: SummaryComponent },
    { path: 'lines',   component: LinesComponent },
    { path: 'history', component: HistoryComponent }
  ]
}

The parent stays mounted — its header, tabs and loaded order stay put — while only the inner view changes. That's what you want for a tabbed detail screen.

pathMatch: 'full' on the empty-path redirect matters: without it, the empty path matches every URL prefix and you get a redirect loop.

Q31

Route guards

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

  canActivate(): boolean | UrlTree {
    return this.auth.isLoggedIn()
      ? true
      : this.router.createUrlTree(['/login']);   // redirect, don't just block
  }
}

{ path: 'admin', component: AdminComponent, canActivate: [AuthGuard] }
GuardAnswers
CanActivateCan you enter this route?
CanActivateChildCan you enter its children?
CanDeactivateCan you leave? — "you have unsaved changes"
ResolveFetch data before the route activates

Returning a UrlTree redirects. Returning false just blocks, which leaves the user staring at the page they were already on with no explanation.

Angular 15+: guards are plain functions — canActivate: [() => inject(AuthService).isLoggedIn()]. Less ceremony, same job.

⚠ A guard is not security

It only stops navigation in the browser. Anyone can call the API directly. The server must check authorisation too — the guard is there for user experience.

Q32

Lazy loading

In one line

A feature is only downloaded when the user first goes there, so the initial bundle stays small.

// module-based
{
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}

// standalone component (Angular 14+)
{
  path: 'reports',
  loadComponent: () => import('./reports/reports.component').then(m => m.ReportsComponent)
}
Without lazy loading With lazy loading main.js 2.4 MB main.js 900 KB ← first paint (everything) admin.js 600 KB ← only if they visit /admin reports.js 400 KB ← only if they visit /reports

Check the numbers in the build output — ng build prints the size of every chunk. The admin area is usually the first thing worth splitting out: most users never open it.

Preloading

RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) keeps the small initial bundle but quietly downloads the rest after startup — so the first click into /admin is instant. Best of both for most apps.