Angular – Routing

July 24, 20263 min readUpdated 8/21/2026

The router maps URLs to components. You give it a table of routes, put an outlet in a template, and it fills the outlet with whatever matches.

provideRouter(
  routes,
<main class="flex-grow-1">
  <router-outlet />
</main>

The route table

{
  path: 'menu',
  title: 'Menu — PizzaHub',
  loadComponent: () => import('./pages/menu/menu-page').then((m) => m.MenuPage),
},

path has no leading slash. title sets the document title on navigation — worth filling in, because otherwise every page in the browser's history is called the same thing.

Order matters

Routes are matched top to bottom, first match wins. The wildcard therefore goes last:

{
  path: '**',
  title: 'Page not found — PizzaHub',
  loadComponent: () => import('./pages/not-found/not-found').then((m) => m.NotFound),
},

Put it anywhere else and it swallows everything below it.

Route parameters

{
  path: 'order-confirmation/:orderId',

The interesting part is how the component receives it. This is configured once at bootstrap:

withComponentInputBinding(),

…and then a matching input() on the routed component is filled in automatically:

readonly type = input<Filter | undefined>(undefined);

That is the menu page receiving ?type=PIZZA as a signal, with no ActivatedRoute injected and no subscription to clean up. It is the tidiest form of React Router's useParams and useSearchParams, and it works for path params, query params and route data alike.

Without it you inject ActivatedRoute and read paramMap, which is still what you will see in most existing code. ⚠️ If you do, remember the route can change without the component being recreated — navigating from /orders/1 to /orders/2 reuses the instance. A value read once in ngOnInit will be stale; the observable form is what handles it.

<a routerLink="/menu" class="btn btn-primary btn-lg">Order now</a>
<a routerLink="/menu" [queryParams]="{ type: 'DRINK' }" class="btn btn-outline-light btn-lg">
  Add a drink
</a>

routerLink rather than href: it navigates within the application instead of reloading the whole page. routerLinkActive adds a class when the route matches, which is how the navbar highlights the current tab.

void this.router.navigate(['/menu'], {
  queryParams: filter === 'ALL' ? {} : { type: filter },
});

The menu's filter tabs put the filter in the URL rather than in component state, deliberately: /menu?type=PIZZA is then shareable, bookmarkable, and survives a refresh. Treating the URL as state is usually the right call for anything a user might want to link to.

replaceUrl

await this.router.navigateByUrl(this.returnUrl(), { replaceUrl: true });

After signing in, this swaps the history entry instead of adding one — so pressing Back does not return the user to the login form they just completed. The same reasoning applies to a guard returning a UrlTree rather than calling navigate().

Scroll position

withInMemoryScrolling({ scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled' }),

Land at the top on a new page; restore the old position on Back. Browsers do this for a document navigation and cannot for a client-side one, so the router has to. It is one line and it is the difference between an app that feels right and one that does not.

Child routes

A route can have children, rendered into an outlet in the parent's template. The admin area is built that way: AdminLayout draws the sidebar and contains its own <router-outlet />, and the six admin screens render inside it. The layout is therefore not repeated on six pages, and — more usefully — a guard on the parent protects all of them.

What is next

That guard, its counterpart on the way out, and the lazy loading in every loadComponent above.