Introduction to Routing
Most non-trivial Angular applications are single-page applications (SPAs): the browser loads one HTML shell, and from then on the framework swaps views in and out without full-page reloads. The Angular Router is the library that makes this possible — it maps URL paths to components, keeps the browser’s address bar in sync, manages browser history, and provides hooks for guarding, resolving, and lazy-loading parts of your app. Getting routing right is foundational, because it shapes how your features are organized and how users move through them.
What the Router does
At its core the Router maintains a mapping between a URL and a tree of components to display. When the URL changes — whether from a link click, a back button, or programmatic navigation — the Router matches the new path against your route configuration, activates the matching components, and renders them into placeholders called router outlets. Because everything happens on the client, navigation is fast and state can be preserved between views.
The Router gives you:
| Capability | Description |
|---|---|
| Declarative config | A Routes array maps paths to components. |
| Outlets | <router-outlet> marks where matched components render. |
| Links | routerLink builds and navigates to URLs declaratively. |
| Parameters | Path, query, and fragment values are exposed as observables/signals. |
| Guards & resolvers | Functional hooks to allow/block navigation and prefetch data. |
| Lazy loading | Load route code on demand to shrink the initial bundle. |
Enabling the Router in a standalone app
Modern Angular (17+) apps are bootstrapped without NgModule. You register the Router by adding provideRouter() to the application providers and passing it your route definitions. Keep the routes in their own file so they stay easy to read as the app grows.
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
export const routes: Routes = [
{ path: '', component: HomeComponent, title: 'Home' },
{ path: 'about', component: AboutComponent, title: 'About us' },
{ path: '**', redirectTo: '' },
];
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
The title property is set automatically as the document title when each route activates — a small built-in feature that saves you from wiring up Title manually.
The
provideRouter()function replaces the olderRouterModule.forRoot(). You can extend it with features such aswithComponentInputBinding(),withViewTransitions(), andwithInMemoryScrolling()— each is a tree-shakable, opt-in piece of behavior.
Rendering routed components
The Router needs somewhere to draw the matched component. That placeholder is <router-outlet>, which you place in your root component’s template. Because standalone components import their own dependencies, remember to import RouterOutlet (and RouterLink if you use links) directly.
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
<nav>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
</nav>
<main>
<router-outlet />
</main>
`,
})
export class AppComponent {}
When you visit /about, the Router matches the second route and renders AboutComponent inside the outlet; the <nav> stays untouched. Clicking the links updates the URL and view without a network round-trip to the server.
Reacting to navigation
Sometimes you need to know when navigation happens — for analytics, scroll handling, or loading indicators. Inject the Router and observe its event stream. You can also read the current URL at any time.
import { Component, inject } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';
@Component({
selector: 'app-shell',
standalone: true,
template: `<router-outlet />`,
})
export class ShellComponent {
private router = inject(Router);
constructor() {
this.router.events
.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
.subscribe((e) => console.log('Navigated to', e.urlAfterRedirects));
}
}
Output:
Navigated to /
Navigated to /about
Best practices
- Keep route definitions in a dedicated
app.routes.tsfile and feature-level*.routes.tsfiles rather than inlining them. - Always include a wildcard route (
path: '**') to handle unknown URLs with a redirect or a 404 component. - Prefer
routerLinkover hardcoded<a href>so navigation stays within the SPA and history works correctly. - Set a
titleon each route for accessible, descriptive document titles out of the box. - Use functional features like
withComponentInputBinding()to bind route params straight to component inputs, keeping components lean. - Order matters: more specific paths must come before less specific ones, and the wildcard route must always be last.