Angular Router Essentials: routerLink, RouterOutlet, Params, Query Params [2026]
Every Angular app with more than one page goes through the Router. It maps URLs to components, handles navigation, parses params, and lets your code react to route changes. This lesson is the on-ramp: a Routes config, a <router-outlet>, navigation by link and by code, reading params, and the few mechanics that catch newcomers.
This is lesson 5.1 of the Angular Tutorial, opening Module 5. After this lesson you can ship a multi-page Angular app. Lessons 5.2–5.6 dive into the rest of the router surface — complete route configuration, guards, events, resolvers, and lazy loading.
The shape #
A router-enabled app needs three things:
provideRouter(routes)inapp.config.ts(lesson 1.6)- A
routesarray declaring path→component mappings - A
<router-outlet />somewhere in your template
// app.routes.ts
import { Routes } from '@angular/router';
import { HomePage } from './pages/home';
import { AboutPage } from './pages/about';
import { NotFoundPage } from './pages/not-found';
export const routes: Routes = [
{ path: '', component: HomePage },
{ path: 'about', component: AboutPage },
{ path: '**', component: NotFoundPage },
];
// 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)],
};
// app.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet, RouterLink],
template: `
<nav>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
</nav>
<router-outlet />
`,
})
export class App {}
Three imports per template that uses the router: RouterOutlet for the slot, RouterLink for declarative navigation, and (later) RouterLinkActive for active-link styling.
<router-outlet /> — where the routed component renders #
When the URL matches a route, the matching component renders INSIDE the <router-outlet />. The outlet is a placeholder — the routed component appears next to it (as a sibling), not inside it.
A typical layout:
<app-root>
<header>...</header>
<main>
<router-outlet />
<!-- routed component renders here, as a sibling of router-outlet -->
</main>
<footer>...</footer>
</app-root>
For nested layouts (a feature shell that itself has sub-pages), nest <router-outlet> inside a routed component — that's how nested routes work (covered with children: config in lesson 5.2).
routerLink — declarative navigation #
Use routerLink instead of href for in-app links. It prevents full-page reloads and works with the router's history.
<a routerLink="/about">About</a>
<a routerLink="/users/42">User 42</a>
<a [routerLink]="['/users', userId()]">User {{ userId() }}</a>
<a [routerLink]="['/posts', postId(), 'edit']">Edit post</a>
Three forms:
- String form:
routerLink="/foo"— a literal path - Array form:
[routerLink]="['/users', id]"— segments concatenated with/ - Relative form:
[routerLink]="['edit']"— relative to the current route
The array form is what you reach for whenever any segment is dynamic.
routerLinkActive — active-link styling #
Apply a CSS class when the link's target matches the current URL:
<a routerLink="/about" routerLinkActive="active">About</a>
For exact-match-only (link active only when URL is exactly /about, not /about/subpage):
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
The exact: true option is almost always what you want for the homepage link, because routerLink="/" would otherwise be "active" on every page (every URL starts with /).
Path parameters #
Declare placeholders with :name syntax:
export const routes: Routes = [
{ path: 'users/:id', component: UserDetailPage },
{ path: 'posts/:id/edit', component: PostEditPage },
];
The component reads them via ActivatedRoute:
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-user-detail',
template: `<h1>User {{ id() }}</h1>`,
})
export class UserDetailPage {
private route = inject(ActivatedRoute);
// params emits whenever ANY param changes; .pipe(map) extracts the one we want
id = toSignal(this.route.params.pipe(map(p => p['id'])));
}
The cleaner v17+ alternative is withComponentInputBinding() — covered next.
withComponentInputBinding() — params as inputs #
When you enable it on the router, every route parameter is bound to a matching input() on the component:
import { provideRouter, withComponentInputBinding } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
],
};
Now:
@Component({
template: `<h1>User {{ id() }}</h1>`,
})
export class UserDetailPage {
id = input.required<string>();
// ↑ automatically receives the value of :id from the URL
}
No ActivatedRoute injection, no toSignal(), no pipe(map(...)). The router writes path params, query params, and the route's data directly into matching component inputs. This is the modern v17+ pattern and what you should use in new code.
Navigating in code — Router.navigate() and navigateByUrl() #
For programmatic navigation (after a form submit, after auth check):
import { Router, inject } from '@angular/router';
export class LoginComponent {
private router = inject(Router);
onSubmit() {
// String form
this.router.navigateByUrl('/dashboard');
// Array form (preferred for dynamic segments)
this.router.navigate(['/users', this.userId()]);
// With query params
this.router.navigate(['/search'], { queryParams: { q: 'angular' } });
// Replace history (no back-button entry)
this.router.navigate(['/login'], { replaceUrl: true });
}
}
The array form has the same semantics as [routerLink] array — segments separated by /, dynamic values mixed in.
Query parameters #
Declare links with query params:
<a [routerLink]="['/search']" [queryParams]="{ q: 'angular', sort: 'date' }">
Search
</a>
Read them with withComponentInputBinding():
@Component({...})
export class SearchPage {
q = input<string | undefined>(); // ?q=...
sort = input<string | undefined>(); // ?sort=...
}
Query params are reactive — when the URL changes (a different ?q=), the input signal updates and the component reacts.
Wildcard route — 404 catch-all #
The ** path matches any URL not matched by an earlier route. Always last in the array:
export const routes: Routes = [
{ path: '', component: HomePage },
{ path: 'about', component: AboutPage },
{ path: '**', component: NotFoundPage }, // matches anything else
];
For a custom 404, route to a NotFoundPage. To redirect 404s back home:
{ path: '**', redirectTo: '/', pathMatch: 'full' }
The pathMatch: 'full' is required when redirectTo is on a non-empty path — covered in lesson 5.2.
Fragment (URL hash) #
Links with #fragment:
<a routerLink="/about" fragment="team">About — team section</a>
Produces /about#team. The router supports withInMemoryScrolling({ scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled' }) to auto-scroll to the matching id on navigation — covered in lesson 5.4 (router events).
A complete tiny app #
// app.routes.ts
import { Routes } from '@angular/router';
import { HomePage } from './pages/home';
import { UserListPage } from './pages/user-list';
import { UserDetailPage } from './pages/user-detail';
import { NotFoundPage } from './pages/not-found';
export const routes: Routes = [
{ path: '', component: HomePage, title: 'Home' },
{ path: 'users', component: UserListPage, title: 'Users' },
{ path: 'users/:id', component: UserDetailPage, title: 'User detail' },
{ path: '**', component: NotFoundPage, title: 'Not found' },
];
// app.config.ts
import { provideRouter, withComponentInputBinding } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes, withComponentInputBinding())],
};
// app.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<nav>
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
<a routerLink="/users" routerLinkActive="active">Users</a>
</nav>
<main>
<router-outlet />
</main>
`,
styles: `nav a.active { font-weight: bold; }`,
})
export class App {}
// pages/user-detail.ts
import { Component, input } from '@angular/core';
@Component({
template: `<h1>User {{ id() }}</h1>`,
})
export class UserDetailPage {
id = input.required<string>(); // bound from :id in URL
}
Four routes, dynamic path param, programmatic-input binding, active-link styling, wildcard 404 — about 40 lines.
The title property #
When you set title: 'Page name' on a route, the router updates document.title automatically. For dynamic titles:
{ path: 'users/:id', component: UserDetailPage, title: (route) => `User ${route.paramMap.get('id')}` }
Replaces the manual document.title = 'X' calls of older Angular code.
Common gotchas #
| Symptom | Cause | Fix |
|---|---|---|
<router-outlet> not rendering |
Missing RouterOutlet in component's imports array |
Add it |
routerLink does nothing |
Missing RouterLink in imports, or the path doesn't match any route |
Check both |
routerLinkActive matches too much |
Default behavior matches sub-paths | Add [routerLinkActiveOptions]="{ exact: true }" |
:id input is undefined |
Forgot withComponentInputBinding() |
Add it to provideRouter(...) features |
| Navigation triggers a full page reload | Used href instead of routerLink |
Switch to routerLink for in-app navigation |
| Wildcard route never matches | Wildcard isn't LAST in the routes array | Move { path: '**', ... } to the end |
What's next #
Lesson 5.2 covers complete route configuration — every property on the Route object (pathMatch, redirectTo, children, outlet, data, resolve, etc.). Lesson 5.3 walks functional route guards (CanActivateFn, CanDeactivateFn). Lesson 5.4 catalogs router events. Lesson 5.5 covers data resolvers and prefetching. Lesson 5.6 closes Module 5 with lazy loading and @defer blocks.
Try it yourself #
A two-page app with dynamic routing:
import { Component, input } from '@angular/core';
import { provideRouter, withComponentInputBinding, RouterOutlet, RouterLink } from '@angular/router';
import { bootstrapApplication } from '@angular/platform-browser';
const Posts = [{id:1,title:'First'},{id:2,title:'Second'},{id:3,title:'Third'}];
@Component({
imports: [RouterLink],
template: `
<h1>Posts</h1>
<ul>@for (p of posts; track p.id) { <li><a [routerLink]="['/posts', p.id]">{{ p.title }}</a></li> }</ul>
`,
})
class ListPage { posts = Posts; }
@Component({
template: `<h1>{{ post()?.title }}</h1><a routerLink="/">Back</a>`,
imports: [RouterLink],
})
class DetailPage {
id = input.required<string>();
post = computed(() => Posts.find(p => p.id === +this.id()));
}
@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: `<router-outlet />`,
})
class App {}
bootstrapApplication(App, {
providers: [provideRouter([
{ path: '', component: ListPage },
{ path: 'posts/:id', component: DetailPage },
], withComponentInputBinding())],
});
Four pages, dynamic param binding, programmatic-input wiring — in a single file.
app.config.ts, makes path/query params and route data flow into components as input() signals, and replaces the older ActivatedRoute.params.pipe(...) dance. Cost: zero unless you have collision between an input name and a route param name (rare). Migration story: legacy ActivatedRoute usage continues to work — opt in to withComponentInputBinding and migrate components opportunistically.Lesson 5.2 picks up the complete route configuration — every property on the Route object.
Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.


