An Angular application is a single HTML page, and the router shows a different component depending on the address. For you as the author of the API this is the same pattern as routing in ASP.NET Core (K2 2.3.2): a table maps addresses to handlers, and parameters come from the path. This lesson sets up the pages of the Kladde (contacts, contact, appointments, booking, 404) with lazy loading, titles, focus after the switch and tests. The data comes from the API in 0202.

As before: project web/kladde-angular, prefix kl, folders src/app/{kontakte,termine,buchung,shared,core} (0101). For brevity, the templates are shown here in the component's code; your files have templateUrl and styleUrl (0102).

State of the checks. The example comes from a throwaway project outside the repository (Node 24.22.3, Angular 22.2.6, TypeScript 6.0.3, Vitest 5.1.0). What ran: ng build (exit code 0) and ng test --no-watch with the routing tests of this lesson (5 tests in app.spec.ts; the final state after 0204 passes 32 tests in 7 files, exit code 0). One test was broken on purpose as a check (line canMatch removed) and turned red. Not run: the dev server in the browser, every manual check with keyboard and screen reader, the navigation against a running API; the tests run in jsdom. Whether Angular offers anything of its own for the focus after navigation, we have not read: open. The statements on guards and resolvers come from the pages of the Angular documentation, read on the day of writing.

The table of routes

The router is registered once in app.config.ts. provideRouter gets the table routes and, for this lesson, the function withComponentInputBinding(); what it is for is shown in the section on path parameters.

src/app/app.config.ts (excerpt):

TypeScript
provideRouter(routes, withComponentInputBinding()),

A route is an object with a path and whatever is shown there. The router checks the entries in the order of the file and takes the first one that matches. That is the same rule by which, in an API, you put a route with a placeholder after the fixed routes. The Kladde's table:

src/app/app.routes.ts:

TypeScript
import { Routes } from '@angular/router';
import { nurGuid } from './core/routing/nur-guid';
import { kontaktLaden } from './kontakte/kontakt-laden';

export const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'kontakte' },
  {
    path: 'kontakte',
    title: 'Contacts',
    loadComponent: () => import('./kontakte/kontakt-liste').then((m) => m.KontaktListe),
  },
  {
    path: 'kontakte/:id',
    title: 'Contact',
    canMatch: [nurGuid],
    resolve: { kontakt: kontaktLaden },
    loadComponent: () => import('./kontakte/kontakt-detail').then((m) => m.KontaktDetail),
  },
  {
    path: 'termine',
    title: 'Appointments',
    loadComponent: () => import('./termine/termine-seite').then((m) => m.TermineSeite),
  },
  {
    path: 'buchung',
    title: 'Book an appointment',
    loadComponent: () => import('./buchung/buchung-seite').then((m) => m.BuchungSeite),
  },
  {
    path: '**',
    title: 'Page not found',
    loadComponent: () => import('./shared/nicht-gefunden').then((m) => m.NichtGefunden),
  },
];

Three things stand out. First, pathMatch: 'full' on the redirect from the root: without it, the empty path would match every address as a prefix. Second, title: the router sets the page title; for screen readers and history it is the first piece of information about where you are. Third, '**' at the very bottom: the route for everything that no other route matches, the application's 404 page. It says nothing about the status code: the server returns the index.html for every address (0403).

Lazy loading with loadComponent

loadComponent gets a function that loads the component's file with import(), only when the route is needed. The build splits the code into files of their own for this: the project's ng build listed five parts under "Lazy chunk files" (kontakt-liste, termine-seite, kontakt-detail, nicht-gefunden, buchung-seite) of 0.3 to 2.1 kB. A small office application gains little from this; the benefit grows with the application, and here it costs nothing.