Angular 22 🅰️ Routing Basics
A single-page application doesn’t have real pages — it swaps views without reloading. Routing is what makes that work. The Angular Router maps URLs to components, renders the matched component in a <router-outlet>, listens for navigation, and keeps the browser’s history in sync. It’s how /users shows the user list, /users/42 shows one user, and /settings shows the settings page — all in the same tab, without a full reload. This chapter covers the basics: routes, outlets, routerLink, programmatic navigation, and the pieces you need for a real app.
Key point: Routing in Angular has three pieces: a route configuration that maps URLs to components, a <router-outlet> where the matched component renders, and navigation — either declarative ([routerLink]) or programmatic (router.navigate()). Standalone apps provide routing with provideRouter(routes). The router reads the current URL, finds the matching route, and renders the component in the outlet. Everything else — parameters, guards, lazy loading — builds on this foundation.
What routing is
Routing maps URLs to components. When the URL changes, the router picks the matching component and renders it.
import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent }
];
Two routes:
/→HomeComponent/about→AboutComponent
Navigate to /about, and Angular renders AboutComponent in the outlet.
The pieces:
| Piece | Purpose |
|---|---|
Routes | Array of route definitions |
provideRouter(routes) | Registers routes with the app |
<router-outlet> | Placeholder where matched components render |
RouterLink | Directive for declarative navigation |
Router | Service for programmatic navigation |
What the router does:
- Reads the current URL
- Matches it against the route config
- Renders the matching component in the outlet
- Listens for link clicks and
navigate()calls - Updates the URL when navigation happens
- Manages browser history (back/forward)
What it doesn’t do:
- Doesn’t reload the page
- Doesn’t fetch data by itself (that’s resolvers)
- Doesn’t decide what’s public (that’s guards)
- Doesn’t optimize loading (that’s lazy loading)
Why routing matters: Without it, every navigation is a full page load. With it, the app stays loaded, state is preserved, and navigation is instant. That’s what makes a SPA feel like an app instead of a website.
Why the router is a separate module: It’s not required for every app. A simple component demo doesn’t need routing. But any app with multiple views does. Keeping the router separate lets you add it when you need it — and keeps the minimal app minimal.
Setting up routing
For a standalone app, routing is provided with provideRouter.
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)]
});
provideRouter(routes) registers the routes with the app. Every component can now use routing features.
The root component hosts the outlet:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `
<header>
<nav>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
</nav>
</header>
<main>
<router-outlet></router-outlet>
</main>
`
})
export class AppComponent {}
The <router-outlet> is where the matched component renders. When the route changes, Angular swaps the component inside the outlet.
Import RouterOutlet and RouterLink: In standalone components, import the directives and services you use.
import { RouterOutlet, RouterLink } from '@angular/router';
@Component({
standalone: true,
imports: [RouterOutlet, RouterLink],
// ...
})
The route file:
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent }
];
A Routes array is just a list of { path, component } objects. The path is the URL segment; the component is what renders.
Why provideRouter: It’s the standalone replacement for RouterModule.forRoot(). It registers the router, the routes, and the directives with the app. Modern apps use it instead of importing RouterModule in a module.
Why the outlet is separate from the routes: The outlet is a placeholder — it appears once in the template, and the router fills it. If the outlet were tied to a route, it couldn’t be reused. Keeping it separate means one outlet can render many routes over time.
Route configuration
Routes are defined as an array of objects. Each object is a route.
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent },
{ path: '**', component: NotFoundComponent }
];
Route properties:
| Property | Purpose |
|---|---|
path | URL segment to match |
component | Component to render |
redirectTo | Redirect to another path |
pathMatch | How to match the path |
children | Nested routes |
loadComponent | Lazy-load a standalone component |
loadChildren | Lazy-load a module |
canActivate | Guards |
resolve | Resolvers |
data | Static data |
Path matching:
path: ''— matches the empty path (root)path: 'about'— matches/aboutpath: 'users/:id'— matches/users/42with a parampath: '**'— wildcard, matches anything
The wildcard route: Should be last. It catches unmatched URLs.
{ path: '**', component: NotFoundComponent }
If it came first, everything would match it.
The empty path — path: '': Matches the root URL.
{ path: '', component: HomeComponent }
pathMatch: 'full': For the empty path, use 'full' to avoid matching every URL.
{ path: '', component: HomeComponent, pathMatch: 'full' }
Without 'full', path: '' would match any URL that starts with the empty string — which is all of them.
Redirects:
{ path: 'home', redirectTo: '', pathMatch: 'full' },
{ path: 'old-about', redirectTo: 'about' }
Navigating to /home redirects to /. Navigating to /old-about redirects to /about.
Lazy loading a standalone component:
{
path: 'settings',
loadComponent: () => import('./settings.component').then(m => m.SettingsComponent)
}
The component loads only when the route is activated.
Lazy loading a route group:
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes)
}
Loads a set of child routes on demand.
Why route order matters: Angular matches routes in order. The first match wins. Put specific routes first, wildcards last. Otherwise, a general route could shadow a specific one.
Why
pathMatch: 'full': By default,path: ''matches any URL starting with the empty string — effectively everything.pathMatch: 'full'requires an exact match of the entire URL. It’s almost always what you want forpath: ''.
<router-outlet> — where components render
The outlet is a placeholder in the template where the router renders the matched component.
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `
<main>
<router-outlet></router-outlet>
</main>
`
})
export class AppComponent {}
When the URL is /about, AboutComponent renders inside the outlet. When the URL is /, HomeComponent renders.
Multiple outlets: You can have more than one outlet by naming them.
<router-outlet></router-outlet> <!-- primary -->
<router-outlet name="sidebar"></router-outlet> <!-- named -->
Named outlets — routes can target a specific outlet.
{
path: 'chat',
component: ChatComponent,
outlet: 'sidebar'
}
Navigate to /(sidebar:chat) and ChatComponent renders in the sidebar outlet while the primary outlet shows the main content.
Nested outlets: With child routes, a child component can have its own outlet.
// Parent template
<router-outlet></router-outlet> <!-- renders child routes -->
// Routes
{
path: 'users',
component: UsersComponent,
children: [
{ path: ':id', component: UserDetailComponent }
]
}
Visiting /users/42 renders UsersComponent in the root outlet, and UserDetailComponent in the UsersComponent‘s outlet.
How the outlet knows what to render: It subscribes to the router’s state. When the URL matches a route, the router emits the matched component. The outlet renders it.
Why the outlet is a directive: It’s imported like any other directive. In standalone components, you add RouterOutlet to imports. It registers itself with the router, and the router fills it.
Why “outlet”: It’s where the routed content comes out. Like a wall outlet provides power, the router outlet provides a place for the routed component. The name is metaphorical but apt.
[routerLink] — declarative navigation
[routerLink] creates a link that navigates without a full page reload.
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
<a [routerLink]="['/users', userId]">Profile</a>
Static path:
<a routerLink="/about">About</a>
Dynamic path with array syntax:
<a [routerLink]="['/users', userId]">User {{ userId }}</a>
The array is joined into a path: ['/users', 42] → /users/42.
Relative links:
<!-- Current URL: /users/42/details -->
<a routerLink="../">Up</a> <!-- → /users/42 -->
<a routerLink="./edit">Edit</a> <!-- → /users/42/edit -->
<a routerLink="../../">Two up</a> <!-- → /users -->
Relative to the current route.
Query parameters and fragments:
<a [routerLink]="['/search']" [queryParams]="{ q: 'hello' }">Search</a>
<a [routerLink]="['/docs']" fragment="section-1">Docs</a>
Renders /search?q=hello and /docs#section-1.
Active link styling: routerLinkActive adds a class when the route is active.
<a routerLink="/about" routerLinkActive="active">About</a>
The active class is added when /about is the current route.
Exact matching:
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
Without exact, the root link would be active on every route. With exact: true, it’s active only on /.
Why routerLink instead of href: href triggers a full page load. routerLink tells the router to navigate — no reload, state preserved, faster.
Why array syntax: It handles special characters and dynamic segments cleanly. ['/users', userId] produces the right URL regardless of what userId contains.
Why
routerLinkActive: Highlighting the current page in navigation is a common need. Without it, you’d manually compare the current URL to each link. The directive does that — and handles edge cases like query params and fragments.
Programmatic navigation
Sometimes navigation happens in code — after a save, on a timeout, in response to a service.
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({ /* ... */ })
export class UserFormComponent {
private router = inject(Router);
save(): void {
this.router.navigate(['/users']);
}
}
Router.navigate() takes an array of path segments.
navigate vs navigateByUrl:
router.navigate(['/users', 42]); // segments → /users/42
router.navigateByUrl('/users/42'); // full URL
navigate takes an array and joins it. navigateByUrl takes a full URL string.
Relative navigation:
router.navigate(['edit'], { relativeTo: this.route });
router.navigate(['../'], { relativeTo: this.route });
relativeTo sets the base for relative navigation.
Query params and fragments:
router.navigate(['/search'], {
queryParams: { q: 'hello' },
fragment: 'results'
});
Replacing history:
router.navigate(['/login'], { replaceUrl: true });
replaceUrl replaces the current history entry — pressing Back won’t return to the previous page.
Navigation with state:
router.navigate(['/confirm'], {
state: { userId: 42 }
});
State isn’t in the URL; it’s stored in the browser history entry. Retrieve it in the target component with history.state.
When to use which:
| Need | Use |
|---|---|
| Link in template | [routerLink] |
| Navigate in code | router.navigate() |
| Full URL string | router.navigateByUrl() |
| Relative | router.navigate([...], { relativeTo }) |
| Replace history | { replaceUrl: true } |
| Query params | { queryParams: {...} } |
Why programmatic navigation matters: Forms, buttons, and side effects often navigate as part of a flow. [routerLink] handles links; navigate() handles logic.
Return value: navigate() returns a Promise<boolean>. true if navigation succeeded, false if a guard blocked it.
const success = await this.router.navigate(['/dashboard']);
if (!success) {
console.log('Navigation blocked');
}
Why promises and not observables: Navigation can be async (lazy loading, guards). The promise resolves when navigation completes or fails. Angular 15+ uses promises for the modern API.
Why both
navigateandnavigateByUrl: They handle different inputs.navigatetakes segments and applies the router’s URL-building rules.navigateByUrltakes a string and parses it.navigateis safer for user input;navigateByUrlis convenient for known URLs.
Reading the current URL
The Router and ActivatedRoute services expose the current URL.
From Router:
import { Router } from '@angular/router';
export class Component {
private router = inject(Router);
getCurrentUrl(): string {
return this.router.url;
}
}
router.url is the current URL as a string.
From ActivatedRoute:
import { ActivatedRoute } from '@angular/router';
export class Component {
private route = inject(ActivatedRoute);
ngOnInit(): void {
console.log(this.route.snapshot.url);
}
}
ActivatedRoute provides the route’s segments, params, query params, fragment, and data.
The two services:
| Service | Provides |
|---|---|
Router | The whole router — navigate, events, config |
ActivatedRoute | The current route — params, data, segments |
Listening to navigation events:
export class AppComponent {
private router = inject(Router);
constructor() {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
console.log('Navigated to', event.url);
}
});
}
}
The router emits events during navigation. NavigationEnd fires when navigation completes.
Navigation lifecycle events:
| Event | When |
|---|---|
NavigationStart | Navigation begins |
RoutesRecognized | Route matched |
GuardsCheckStart / End | Guards run |
ResolveStart / End | Resolvers run |
NavigationEnd | Navigation completed |
NavigationCancel | Navigation cancelled |
NavigationError | Navigation failed |
Why reading the URL matters: Components often need to know where they are — to highlight the current section, log analytics, or react to changes. The router and ActivatedRoute provide that.
Snapshot vs observable: route.snapshot gives the current value; route.params, route.queryParams, and route.data are observables that emit on change.
// Snapshot — value at activation
const id = this.route.snapshot.paramMap.get('id');
// Observable — updates on change
this.route.paramMap.subscribe(params => {
const id = params.get('id');
});
Use the observable when the same component may be reused with different params.
Why the difference: Navigating from /users/1 to /users/2 may reuse the same component instance. The snapshot won’t update; the observable will. Subscribe to react.
Why both snapshot and observable: Snapshot is convenient for one-time reads. Observable is necessary when params can change without the component being recreated. Both have their place.
A full example
A small app with three routes and navigation.
// ============================================
// COMPONENTS
// ============================================
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-home',
standalone: true,
template: `
<h1>Welcome</h1>
<p>This is the home page.</p>
`
})
export class HomeComponent {}
@Component({
selector: 'app-about',
standalone: true,
template: `
<h1>About</h1>
<p>Learn more about us.</p>
`
})
export class AboutComponent {}
@Component({
selector: 'app-contact',
standalone: true,
template: `
<h1>Contact</h1>
<p>Get in touch.</p>
`
})
export class ContactComponent {}
@Component({
selector: 'app-not-found',
standalone: true,
template: `
<h1>404</h1>
<p>Page not found.</p>
`
})
export class NotFoundComponent {}
// ============================================
// ROUTES
// ============================================
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent },
{ path: '**', component: NotFoundComponent }
];
// ============================================
// ROOT COMPONENT
// ============================================
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<header>
<nav>
<a
routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }">
Home
</a>
<a routerLink="/about" routerLinkActive="active">About</a>
<a routerLink="/contact" routerLinkActive="active">Contact</a>
</nav>
</header>
<main>
<router-outlet></router-outlet>
</main>
`,
styles: `
nav a.active {
font-weight: bold;
color: blue;
}
`
})
export class AppComponent {}
// ============================================
// BOOTSTRAP
// ============================================
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)]
});
What this shows:
- Four components, each with a template
- Routes mapping paths to components
pathMatch: 'full'on the root route- Wildcard route for 404s
[routerLink]for navigationrouterLinkActivefor highlightingprovideRouterfor bootstrap
Navigate between pages — no reload, state preserved.
Why this shape: It’s the minimal viable router app. Every piece is present: routes, outlet, links, active state, wildcard. That structure scales — nested routes, parameters, guards all build on it.
Complete Example Session
# ============================================
# PART 1: HOME COMPONENT
# ============================================
cat > home.component.ts << 'EOF'
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
standalone: true,
template: `<h1>Home</h1>`
})
export class HomeComponent {}
EOF
# ============================================
# PART 2: ABOUT COMPONENT
# ============================================
cat > about.component.ts << 'EOF'
import { Component } from '@angular/core';
@Component({
selector: 'app-about',
standalone: true,
template: `<h1>About</h1>`
})
export class AboutComponent {}
EOF
# ============================================
# PART 3: NOT FOUND
# ============================================
cat > not-found.component.ts << 'EOF'
import { Component } from '@angular/core';
@Component({
selector: 'app-not-found',
standalone: true,
template: `<h1>404</h1>`
})
export class NotFoundComponent {}
EOF
# ============================================
# PART 4: ROUTES
# ============================================
cat > app.routes.ts << 'EOF'
import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { NotFoundComponent } from './not-found.component';
export const routes: Routes = [
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'about', component: AboutComponent },
{ path: '**', component: NotFoundComponent }
];
EOF
npx tsc --noEmit app.routes.ts
# (no errors)
# ============================================
# PART 5: ROOT COMPONENT
# ============================================
cat > app.component.ts << 'EOF'
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<nav>
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
<a routerLink="/about" routerLinkActive="active">About</a>
</nav>
<router-outlet></router-outlet>
`
})
export class AppComponent {}
EOF
npx tsc --noEmit app.component.ts
# (no errors)
# ============================================
# PART 6: BOOTSTRAP
# ============================================
cat > main.ts << 'EOF'
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app.component';
import { routes } from './app.routes';
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)]
});
EOF
npx tsc --noEmit main.ts
# (no errors)
# ============================================
# PART 7: PROGRAMMATIC NAVIGATION
# ============================================
cat > form.component.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-form',
standalone: true,
template: `<button (click)="save()">Save</button>`
})
export class FormComponent {
private router = inject(Router);
save(): void {
// ... save logic
this.router.navigate(['/about']);
}
}
EOF
npx tsc --noEmit form.component.ts
# (no errors)
# ============================================
# PART 8: READING URL
# ============================================
cat > url.component.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';
@Component({
selector: 'app-url',
standalone: true,
template: `<p>{{ currentUrl }}</p>`
})
export class UrlComponent {
private router = inject(Router);
currentUrl = this.router.url;
constructor() {
this.router.events
.pipe(filter(e => e instanceof NavigationEnd))
.subscribe(() => {
this.currentUrl = this.router.url;
});
}
}
EOF
npx tsc --noEmit url.component.ts
# (no errors)
# ============================================
# PART 9: SUMMARY
# ============================================
cat << 'EOF'
Router structure:
main.ts
└── provideRouter(routes)
app.routes.ts
└── Routes array
app.component.ts
├── RouterOutlet ← where routes render
├── RouterLink ← declarative navigation
└── RouterLinkActive ← active state
Feature components
└── rendered by the outlet
Navigation:
[routerLink]="'/path'" → static
[routerLink]="['/path', id]" → dynamic
router.navigate(['/path']) → programmatic
router.navigateByUrl('/path') → full URL
EOF
Quick Reference
Setup
| Step | Code |
|---|---|
| Import | import { provideRouter } from '@angular/router'; |
| Provide | providers: [provideRouter(routes)] |
| Define routes | export const routes: Routes = [...] |
| Outlet | <router-outlet></router-outlet> |
| Link | <a routerLink="/path"> |
Route Properties
| Property | Purpose |
|---|---|
path | URL segment |
component | Component to render |
redirectTo | Redirect target |
pathMatch | 'prefix' or 'full' |
children | Nested routes |
loadComponent | Lazy-load component |
loadChildren | Lazy-load routes |
canActivate | Guard |
resolve | Resolver |
data | Static data |
outlet | Named outlet |
Path Matching
| Path | Matches |
|---|---|
'' | Empty (root) |
'about' | /about |
'users/:id' | /users/42 |
'**' | Anything (wildcard) |
'a/b' | /a/b (nested in path) |
pathMatch
| Value | Meaning |
|---|---|
'prefix' | Default — URL starts with path |
'full' | URL exactly equals path |
routerLink Forms
| Form | Result |
|---|---|
routerLink="/about" | Static path |
[routerLink]="['/users', id]" | Dynamic segments |
[routerLink]="['../']" | Relative |
[routerLink]="['/search']" [queryParams]="{...}" | With query |
[routerLink]="['/docs']" fragment="section" | With fragment |
routerLinkActive
| Usage | Effect |
|---|---|
routerLinkActive="active" | Adds class when active |
[routerLinkActiveOptions]="{ exact: true }" | Exact match only |
[routerLinkActiveOptions]="{ exact: false }" | Prefix match (default) |
Programmatic Navigation
| Method | Input |
|---|---|
router.navigate(['/path']) | Segments array |
router.navigateByUrl('/path') | Full URL |
router.navigate(['../'], { relativeTo }) | Relative |
router.navigate(['/x'], { queryParams }) | With query |
router.navigate(['/x'], { replaceUrl: true }) | Replace history |
router.navigate(['/x'], { state }) | With state |
Router Services
| Service | Provides |
|---|---|
Router | Navigation, events, URL |
ActivatedRoute | Current route info |
ActivatedRoute Properties
| Property | Type |
|---|---|
snapshot | Current route snapshot |
params | Observable of params |
queryParams | Observable of query params |
data | Observable of static data |
url | Observable of URL segments |
fragment | Observable of fragment |
Snapshot vs Observable
| Use | When |
|---|---|
| Snapshot | One-time read |
| Observable | Reacts to changes |
Navigation Events
| Event | When |
|---|---|
NavigationStart | Begins |
RoutesRecognized | Matched |
GuardsCheckStart | Guards begin |
GuardsCheckEnd | Guards done |
ResolveStart | Resolvers begin |
ResolveEnd | Resolvers done |
NavigationEnd | Completed |
NavigationCancel | Cancelled |
NavigationError | Failed |
Directives and Services
| Import | Purpose |
|---|---|
RouterOutlet | Render matched component |
RouterLink | Declarative navigation |
RouterLinkActive | Active state |
Router | Programmatic navigation |
ActivatedRoute | Current route info |
provideRouter | Register routes |
Error Cases
| Error | Cause |
|---|---|
Cannot match any routes | No matching route |
Cannot activate an already activated outlet | Nested outlet config |
Invalid configuration | Wrong route shape |
NullInjectorError: Router | Forgot provideRouter |
Common Patterns
| Pattern | Code |
|---|---|
| Root route | { path: '', component: Home, pathMatch: 'full' } |
| Wildcard | { path: '**', component: NotFound } |
| Redirect | { path: 'old', redirectTo: 'new', pathMatch: 'full' } |
| Lazy component | { path: 'x', loadComponent: () => import(...) } |
| Lazy routes | { path: 'x', loadChildren: () => import(...) } |
| Named outlet | { path: 'x', component: C, outlet: 'sidebar' } |
Best Practices
✅ Do This:
// Use provideRouter for standalone apps
providers: [provideRouter(routes)] // ✅
// Use pathMatch: 'full' on empty path
{ path: '', component: Home, pathMatch: 'full' } // ✅
// Put wildcard last
{ path: '**', component: NotFound } // ✅
// Use routerLink in templates
<a routerLink="/about">About</a> // ✅
// Use array syntax for dynamic paths
<a [routerLink]="['/users', userId]">Profile</a> // ✅
// Use routerLinkActive for current state
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a> // ✅
// Use router.navigate in code
this.router.navigate(['/users']); // ✅
// Use navigateByUrl for known URLs
this.router.navigateByUrl('/settings'); // ✅
// Use replaceUrl to avoid back-button surprises
this.router.navigate(['/login'], { replaceUrl: true }); // ✅
// Subscribe to params when the component may be reused
this.route.paramMap.subscribe(params => { /* ... */ }); // ✅
❌ Don’t Do This:
// Don't forget provideRouter
bootstrapApplication(App, { providers: [] }); // ⚠️ no routing // ⚠️
// Don't omit pathMatch on empty path
{ path: '', component: Home } // ⚠️ matches everything // ⚠️
// Don't put wildcard first
{ path: '**', component: NotFound },
{ path: 'about', component: About } // ⚠️ never reached // ⚠️
// Don't use href for internal links
<a href="/about">About</a> // ⚠️ full page reload // ⚠️
// Don't forget to import RouterOutlet
@Component({ imports: [] }) // ⚠️ outlet won't work // ⚠️
// Don't use snapshot if params change
const id = this.route.snapshot.paramMap.get('id');
// ⚠️ won't update on /users/1 → /users/2 // ⚠️
// Don't navigate to hardcoded strings in templates
<a routerLink="/Users"> // ⚠️ case-sensitive, might not match // ⚠️
// Don't ignore navigation errors
this.router.navigate(['/x']); // ⚠️ handle the promise // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing provideRouter | No router injection | Add to providers |
Missing pathMatch | Root matches all | Use 'full' |
| Wildcard first | Unreachable routes | Move wildcard last |
href instead of routerLink | Full reload | Use routerLink |
| Snapshot with changing params | Stale value | Subscribe to observable |
Forgot to import RouterOutlet | Outlet doesn’t work | Add to imports |
| Case mismatch in path | Route doesn’t match | Match exactly |
| Relative link confusion | Wrong target | Understand ../ and ./ |
Navigation in ngOnInit | Component may not be ready | Use effects or guards |
| Multiple outlets unnamed | Wrong target | Name them |
Real-World Examples
1. Basic route
{ path: 'about', component: AboutComponent }
2. Root route
{ path: '', component: HomeComponent, pathMatch: 'full' }
3. Wildcard
{ path: '**', component: NotFoundComponent }
4. Redirect
{ path: 'old', redirectTo: 'new', pathMatch: 'full' }
5. Lazy component
{ path: 'admin', loadComponent: () => import('./admin.component').then(m => m.AdminComponent) }
6. Lazy routes
{ path: 'shop', loadChildren: () => import('./shop/shop.routes').then(m => m.shopRoutes) }
7. routerLink static
<a routerLink="/about">About</a>
8. routerLink dynamic html
<a [routerLink]="['/users', userId]">Profile</a>
9. Query params
<a [routerLink]="['/search']" [queryParams]="{ q: 'x' }">Search</a>
10. Fragment
<a [routerLink]="['/docs']" fragment="intro">Docs</a>
11. Active link
<a routerLink="/about" routerLinkActive="active">About</a>
12. Exact active match
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
13. Programmatic navigation
this.router.navigate(['/users']);
14. Navigate by URL
this.router.navigateByUrl('/users/42');
15. Relative navigation
this.router.navigate(['edit'], { relativeTo: this.route });
16. Navigate with state
this.router.navigate(['/confirm'], { state: { userId: 42 } });
17. Replace history
this.router.navigate(['/login'], { replaceUrl: true });
18. Read current URL
const url = this.router.url;
19. Navigate on route events
this.router.events.subscribe(e => {
if (e instanceof NavigationEnd) console.log('navigated');
});
20. Read route params (reactive)
this.route.paramMap.subscribe(params => {
const id = params.get('id');
});
Visual: Router Flow
┌──────────────────────────────────────────────┐
│ User clicks <a routerLink="/about"> │
│ │ │
│ ▼ │
│ Router intercepts the click │
│ │ │
│ ▼ │
│ Matches route config │
│ │ │
│ ▼ │
│ Loads component (if lazy) │
│ │ │
│ ▼ │
│ Renders in <router-outlet> │
│ │ │
│ ▼ │
│ Updates browser URL │
│ │ │
│ ▼ │
│ Adds history entry │
│ │
└──────────────────────────────────────────────┘
Visual: Router Setup
┌──────────────────────────────────────────────┐
│ main.ts │
│ ─ bootstrapApplication(App, { │
│ providers: [provideRouter(routes)] │
│ }) │
│ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ app.routes.ts │
│ ─ export const routes: Routes = [ │
│ { path: '', component: Home }, │
│ { path: 'about', component: About } │
│ ] │
│ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ app.component.ts │
│ ─ imports: [RouterOutlet, RouterLink] │
│ ─ template: │
│ <a routerLink="/about">About</a> │
│ <router-outlet></router-outlet> │
│ │
└──────────────────────────────────────────────┘
Visual: Path Matching
┌──────────────────────────────────────────────┐
│ Routes: │
│ 1. { path: 'about', component: About } │
│ 2. { path: '', component: Home, pathMatch: 'full' }│
│ 3. { path: '**', component: NotFound } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ URL /about │
│ → matches route 1 │
│ │
│ URL / │
│ → doesn't match 1 │
│ → matches 2 (full) │
│ │
│ URL /other │
│ → doesn't match 1 or 2 │
│ → matches 3 (wildcard) │
│ │
└──────────────────────────────────────────────┘
Visual: pathMatch: 'full'
┌──────────────────────────────────────────────┐
│ Without pathMatch: │
│ │
│ { path: '', component: Home } │
│ │
│ URL /about │
│ → '' is a prefix of '/about' │
│ → matches Home │
│ → /about unreachable! │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With pathMatch: 'full': │
│ │
│ { path: '', component: Home, pathMatch: 'full' }│
│ │
│ URL /about │
│ → '' doesn't fully match '/about' │
│ → doesn't match Home │
│ │
│ URL / │
│ → '' fully matches '/' │
│ → matches Home │
│ │
└──────────────────────────────────────────────┘
Visual: RouterLink Forms
┌──────────────────────────────────────────────┐
│ Static: │
│ <a routerLink="/about"> │
│ → /about │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Dynamic: │
│ <a [routerLink]="['/users', 42]"> │
│ → /users/42 │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Relative: │
│ <a routerLink="../"> │
│ → one level up │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With query: │
│ <a [routerLink]="['/search']" [queryParams]="{q:'x'}">│
│ → /search?q=x │
│ │
└──────────────────────────────────────────────┘
Visual: Active Link
┌──────────────────────────────────────────────┐
│ Navigation: │
│ │
│ <a routerLink="/" routerLinkActive="active" │
│ [routerLinkActiveOptions]="{ exact: true }">Home</a>│
│ <a routerLink="/about" routerLinkActive="active">About</a>│
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ URL = / │
│ Home link → has class "active" │
│ About link → no class │
│ │
│ URL = /about │
│ Home link → no class (exact: true) │
│ About link → has class "active" │
│ │
└──────────────────────────────────────────────┘
Visual: Outlet Rendering
┌──────────────────────────────────────────────┐
│ App template: │
│ │
│ <nav>...</nav> │
│ <router-outlet></router-outlet> │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ URL = / │
│ │
│ <nav>...</nav> │
│ <app-home></app-home> ← rendered │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ URL = /about │
│ │
│ <nav>...</nav> │
│ <app-about></app-about> ← swapped │
│ │
└──────────────────────────────────────────────┘
Visual: Programmatic Navigation
┌──────────────────────────────────────────────┐
│ export class FormComponent { │
│ private router = inject(Router); │
│ │
│ save() { │
│ // logic │
│ this.router.navigate(['/users']); │
│ } │
│ } │
│ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Router parses ['/users'] │
│ Builds URL /users │
│ Matches route │
│ Renders component │
│ │
└──────────────────────────────────────────────┘
Visual: Navigation Events
┌──────────────────────────────────────────────┐
│ NavigationStart │
│ │ │
│ ▼ │
│ RoutesRecognized │
│ │ │
│ ▼ │
│ GuardsCheckStart │
│ GuardsCheckEnd │
│ │ │
│ ▼ │
│ ResolveStart │
│ ResolveEnd │
│ │ │
│ ▼ │
│ NavigationEnd ✅ │
│ │
│ Or NavigationCancel / NavigationError │
│ │
└──────────────────────────────────────────────┘
Visual: Reading Params
┌──────────────────────────────────────────────┐
│ Route: { path: 'users/:id' } │
│ │
│ URL: /users/42 │
│ │
│ ActivatedRoute: │
│ ├── snapshot.paramMap.get('id') → '42' │
│ └── paramMap.subscribe(...) → emits │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ URL: /users/42 → /users/43 │
│ │
│ Component may be reused │
│ │
│ snapshot stays '42' │
│ observable emits '43' ✅ │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Setting up routing? │
│ └── provideRouter(routes) │
│ │
│ Defining a route? │
│ └── { path, component } │
│ │
│ Root route? │
│ └── pathMatch: 'full' │
│ │
│ Unknown URL? │
│ └── { path: '**', component: NotFound }│
│ │
│ Link in template? │
│ └── routerLink │
│ │
│ Highlight current? │
│ └── routerLinkActive │
│ │
│ Navigate in code? │
│ └── router.navigate() │
│ │
│ Read params? │
│ ├── Once ──► snapshot │
│ └── Ongoing ──► observable │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| Routing | Maps URLs to components |
Routes | Array of route definitions |
provideRouter | Registers routes |
<router-outlet> | Where components render |
routerLink | Declarative navigation |
routerLinkActive | Active link styling |
Router.navigate | Programmatic navigation |
ActivatedRoute | Current route info |
pathMatch: 'full' | Exact path matching |
| Wildcard route | ** catches unmatched URLs |
Key takeaways:
- Routing maps URLs to components, swapping views without reloading
provideRouter(routes)registers routing in standalone appsRoutesis an array of{ path, component }definitions<router-outlet>marks where the matched component rendersrouterLinknavigates without a full page reload — prefer it overhrefrouterLinkActiveadds a class when a link’s route is active- Use
pathMatch: 'full'on the root route to prevent it matching everything - Put the wildcard route (
**) last to catch unmatched URLs - Redirects —
{ path: 'old', redirectTo: 'new' }— remap old URLs - Lazy loading —
loadComponentandloadChildren— loads routes on demand - Programmatic navigation —
router.navigate([...])— for code-driven flows - Relative navigation —
{ relativeTo: this.route }— for nested paths ActivatedRoutegives params, query params, and data;Routergives the whole state- Use snapshot for one-time reads; observable when params can change
- Navigation events —
NavigationEnd,NavigationCancel,NavigationError— let you react to routing
Remember: Routing is what turns a page into an app. Routes map URLs to components. The outlet renders them. routerLink navigates without reloading. router.navigate() does it from code. Get the basics right — routes, outlet, links, active state — and the rest (parameters, guards, lazy loading) builds on top. Every Angular app with more than one view needs routing, and these are the pieces that make it work.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!