Angular 24 🅰️ Nested Routes
Real apps have hierarchies — a settings page with sub-pages, a dashboard with panels, a shop with categories and products. Flattening all of those into top-level routes would produce hundreds of entries and duplicate the shared layout. Nested routes solve this: child routes render inside a parent’s <router-outlet>, sharing the parent’s template, layout, and state. The URL becomes hierarchical (/settings/profile), but the routing config mirrors the page structure. This chapter covers how nested routes work, the child route syntax, multiple outlets per level, and the patterns that keep complex navigation manageable.
Key point: A child route is defined in a parent route’s children array. The parent component’s template includes a <router-outlet> where the child renders. The URL is the parent path plus the child path — /parent/child. The parent stays mounted while children change. That means shared layout (headers, sidebars, tabs) lives once in the parent, and children only handle what’s specific to them.
What nested routes are
A nested route — also called a child route — is a route defined inside another route.
export const routes: Routes = [
{
path: 'settings',
component: SettingsComponent,
children: [
{ path: 'profile', component: ProfileComponent },
{ path: 'account', component: AccountComponent },
{ path: 'notifications', component: NotificationsComponent }
]
}
];
Three routes:
/settings/profile/settings/account/settings/notifications
Each renders SettingsComponent in the root outlet, and the child component in the SettingsComponent‘s outlet.
The structure:
URL: /settings/profile
Root outlet → SettingsComponent
└── Child outlet → ProfileComponent
Why this matters:
- Shared layout —
SettingsComponentrenders its header, sidebar, tabs once - Only children change — clicking between profile/account swaps only the inner view
- URL hierarchy —
/settings/profilereads naturally - Config hierarchy — the routes mirror the page structure
What the parent needs: A <router-outlet> in its template. Without it, the child route matches but nothing renders.
@Component({
selector: 'app-settings',
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
<h1>Settings</h1>
<nav>
<a routerLink="profile">Profile</a>
<a routerLink="account">Account</a>
<a routerLink="notifications">Notifications</a>
</nav>
<router-outlet></router-outlet>
`
})
export class SettingsComponent {}
The links are relative — profile not /settings/profile. The router resolves them against the current route.
Why relative links: Relative to the parent’s route. routerLink="profile" becomes /settings/profile when the parent is at /settings. Absolute links would work too, but relative links move with the parent if the path changes.
Why nest at all: A flat config duplicates the parent’s layout for each child. With nesting, the layout lives once and children fill in the content. It’s composition at the routing level — the parent provides the frame, the children provide the content.
Child route configuration
Child routes go in the children array of a parent route.
{
path: 'settings',
component: SettingsComponent,
children: [
{ path: '', component: SettingsHomeComponent },
{ path: 'profile', component: ProfileComponent },
{ path: 'account', component: AccountComponent },
{ path: '**', component: SettingsNotFoundComponent }
]
}
The empty child path: path: '' matches the parent’s URL exactly — /settings.
{ path: '', component: SettingsHomeComponent }
Visiting /settings renders SettingsComponent and SettingsHomeComponent.
The wildcard in children: A ** inside a parent only matches URLs that start with the parent’s path. /settings/anything matches **; /other doesn’t.
{ path: '**', component: SettingsNotFoundComponent }
Child routes can have their own children: Nesting is arbitrary depth.
{
path: 'admin',
component: AdminComponent,
children: [
{
path: 'users',
component: UsersComponent,
children: [
{ path: ':id', component: UserDetailComponent }
]
}
]
}
URL: /admin/users/42. Renders AdminComponent → UsersComponent → UserDetailComponent, each in their own outlet.
Reading params at each level: Each ActivatedRoute reads from its own level.
// AdminComponent: route.parent for its level
// UsersComponent: route for its level
// UserDetailComponent: route for its level, parent chain for ancestors
Each component’s ActivatedRoute gives its own params. To read a parent’s, walk the parent chain.
Child route data: Set data on any level.
{
path: 'settings',
component: SettingsComponent,
data: { title: 'Settings' },
children: [
{ path: 'profile', component: ProfileComponent, data: { title: 'Profile' } }
]
}
The data is merged from parent to child — a child inherits the parent’s data.
Why the parent’s data flows down: Nested routes form a chain. Each level can add to the data; the child sees everything the ancestors set. Useful for titles, breadcrumbs, and permissions.
Why the empty child path is common: The parent URL is a valid page —
/settingsshould show something. The empty child path provides that default view. Without it,/settingsmatches the parent but nothing renders in the child outlet.
Rendering: nested outlets
Each level of nesting needs its own <router-outlet>.
// AppComponent (root)
template: `
<nav>...</nav>
<router-outlet></router-outlet> <!-- SettingsComponent renders here -->
`
// SettingsComponent
template: `
<aside>...</aside>
<router-outlet></router-outlet> <!-- ProfileComponent renders here -->
`
Two outlets, two levels. Angular renders the parent in the first, the child in the second.
The rendering tree:
<app-root>
<router-outlet> ← root outlet
<app-settings>
<router-outlet> ← child outlet
<app-profile></app-profile>
</router-outlet>
</app-settings>
</router-outlet>
</app-root>
Each level wraps the next. The URL determines what renders at each level.
Named outlets: A parent can have multiple outlets with names.
<router-outlet></router-outlet>
<router-outlet name="sidebar"></router-outlet>
Named routes target the secondary outlet.
{
path: 'chat',
component: ChatComponent,
outlet: 'sidebar'
}
Navigate to /(sidebar:chat) and ChatComponent renders in the sidebar outlet.
Nested + named: The combination is powerful but rare. Each level can have its own named outlets.
Why each level needs an outlet: The outlet is where the router renders. Without it, the route matches but nothing appears. Every nested level in the config needs a matching outlet in the parent’s template.
Why the tree matters: The DOM mirrors the routing tree. Parent components stay mounted; only the child’s outlet swaps its content. That’s how shared layout stays alive while inner views change.
Why outlets are position-based: An outlet in a template marks a place. The router fills the nearest outlet in the route chain. Nesting outlets gives the router multiple places to fill, one per level.
Relative navigation
Nested routes make links relative to the current route.
Relative links:
<!-- In SettingsComponent at /settings -->
<a routerLink="profile">Profile</a> <!-- → /settings/profile -->
<a routerLink="account">Account</a> <!-- → /settings/account -->
Relative to child:
<!-- In ProfileComponent at /settings/profile -->
<a routerLink="../account">Account</a> <!-- → /settings/account -->
<a routerLink="edit">Edit</a> <!-- → /settings/profile/edit -->
../ goes up one level; a name goes down.
Relative navigation in code:
this.router.navigate(['../account'], { relativeTo: this.route });
this.router.navigate(['edit'], { relativeTo: this.route });
relativeTo sets the base. Without it, navigate treats the path as absolute.
Preserving query params when navigating children:
this.router.navigate(['edit'], {
relativeTo: this.route,
queryParamsHandling: 'preserve'
});
Why relative matters: Nested routes have deep URLs. Absolute links would need the full path every time. Relative links use the current route as the base — shorter, and they move with the parent.
Why relativeTo: The router needs to know what to be relative to. In a template, it’s the current route. In code, relativeTo: this.route supplies it. Without relativeTo, navigate(['edit']) would try to navigate to a top-level /edit.
Why relative links are the default in nested contexts: A parent template knows where it is. Links relative to that location express “where to go from here” — the natural mental model for nested navigation. Absolute links are the exception, used when jumping to an unrelated part of the app.
Shared layout pattern
The most common use of nested routes is shared layout.
export const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: '', component: OverviewComponent },
{ path: 'analytics', component: AnalyticsComponent },
{ path: 'reports', component: ReportsComponent },
{ path: 'settings', component: SettingsComponent }
]
}
];
DashboardComponent renders the shell — header, sidebar, navigation. Each child renders the content.
The parent template:
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="dashboard">
<aside class="sidebar">
<a routerLink="/dashboard" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Overview</a>
<a routerLink="/dashboard/analytics" routerLinkActive="active">Analytics</a>
<a routerLink="/dashboard/reports" routerLinkActive="active">Reports</a>
<a routerLink="/dashboard/settings" routerLinkActive="active">Settings</a>
</aside>
<main class="content">
<router-outlet></router-outlet>
</main>
</div>
`
})
export class DashboardComponent {}
The sidebar and content layout stays while children change.
What stays mounted: DashboardComponent never unmounts while you’re in /dashboard/*. Its state, subscriptions, and DOM persist.
What changes: The child in the outlet. Navigating from overview to analytics destroys OverviewComponent and creates AnalyticsComponent.
Why this matters: Shared layout doesn’t re-render on every navigation. State in the parent persists. Animations on the parent (like a sidebar that stays open) aren’t restarted.
Why it’s more efficient: Recreating the entire layout on every navigation would be wasteful. Nesting keeps the outer frame and swaps only the inner content — the SPA equivalent of a partial page update.
Common layout patterns:
- Sidebar + content — dashboard, admin
- Tabs + content — settings, account
- Master + detail — list on the left, item on the right
- Header + body — documentation, e-commerce
Each maps naturally to a parent route with children.
Why layout patterns use nested routes: The layout is the point. A sidebar exists to navigate children — the children are the content. Separating them into parent and child routes matches the structure.
Why the parent persists: Angular only recreates a component when its route changes to a different component. As long as the URL stays under
/dashboard,DashboardComponentis reused. Only the child route changes.
Data resolution at each level
Resolvers run for the level they’re on. Data flows down.
{
path: 'users',
component: UsersComponent,
resolve: { users: usersResolver },
children: [
{
path: ':id',
component: UserDetailComponent,
resolve: { user: userResolver }
}
]
}
usersResolver runs when /users is activated. userResolver runs when a child is activated. The parent’s resolved data is available through the route’s parent chain.
Reading resolved data:
// In UsersComponent
const users = this.route.snapshot.data['users'];
// In UserDetailComponent
const user = this.route.snapshot.data['user'];
const users = this.route.parent?.snapshot.data['users'];
Each component reads its own data. route.parent accesses the parent’s data.
Resolving at the parent vs the child: Resolve data at the level it’s needed. If the child needs the parent’s data, resolve at the parent. If only the child needs it, resolve at the child.
Why resolvers run per level: Resolvers are tied to routes. A route activates → its resolvers run. Nested routes activate together — parent resolvers run before child resolvers. That’s the order: parent first, then child.
Order of resolution:
- Parent’s guards
- Parent’s resolvers
- Child’s guards
- Child’s resolvers
- Both components created
If a guard or resolver fails, the whole chain fails.
Why this order: The parent must be ready before the child. If the parent needs to load data, the child can depend on it. The router runs them in sequence.
Why resolvers are useful at nested levels: The parent loads shared data (like the list). The child loads its own (like the selected item). Each level does its part, and children can read the parent’s data if needed.
A full example
An admin dashboard with nested routes.
// ============================================
// SERVICES
// ============================================
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers(): Promise<User[]> {
return this.http.get<User[]>('/api/users').toPromise() as Promise<User[]>;
}
getUser(id: number): Promise<User> {
return this.http.get<User>(`/api/users/${id}`).toPromise() as Promise<User>;
}
}
// ============================================
// RESOLVERS
// ============================================
import { ResolveFn } from '@angular/router';
export const usersResolver: ResolveFn<User[]> = () => {
return inject(UserService).getUsers();
};
export const userResolver: ResolveFn<User> = (route) => {
const id = Number(route.paramMap.get('id'));
return inject(UserService).getUser(id);
};
// ============================================
// ADMIN SHELL
// ============================================
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-admin',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="admin">
<aside class="sidebar">
<h2>Admin</h2>
<nav>
<a routerLink="users" routerLinkActive="active">Users</a>
<a routerLink="settings" routerLinkActive="active">Settings</a>
<a routerLink="logs" routerLinkActive="active">Logs</a>
</nav>
</aside>
<main class="content">
<router-outlet></router-outlet>
</main>
</div>
`
})
export class AdminComponent {}
// ============================================
// USERS LIST
// ============================================
import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
@Component({
selector: 'app-users',
standalone: true,
imports: [RouterLink, RouterOutlet],
template: `
<h2>Users</h2>
<ul>
@for (user of users; track user.id) {
<li>
<a [routerLink]="[user.id]" routerLinkActive="active">
{{ user.name }}
</a>
</li>
}
</ul>
<router-outlet></router-outlet>
`
})
export class UsersComponent {
private route = inject(ActivatedRoute);
users = this.route.snapshot.data['users'] as User[];
}
// ============================================
// USER DETAIL
// ============================================
@Component({
selector: 'app-user-detail',
standalone: true,
template: `
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
`
})
export class UserDetailComponent {
private route = inject(ActivatedRoute);
user = this.route.snapshot.data['user'] as User;
}
// ============================================
// OTHER PAGES
// ============================================
@Component({
selector: 'app-settings',
standalone: true,
template: `<h2>Settings</h2>`
})
export class SettingsComponent {}
@Component({
selector: 'app-logs',
standalone: true,
template: `<h2>Logs</h2>`
})
export class LogsComponent {}
// ============================================
// ROUTES
// ============================================
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'admin', pathMatch: 'full' },
{
path: 'admin',
component: AdminComponent,
children: [
{ path: '', redirectTo: 'users', pathMatch: 'full' },
{ path: 'settings', component: SettingsComponent },
{ path: 'logs', component: LogsComponent },
{
path: 'users',
component: UsersComponent,
resolve: { users: usersResolver },
children: [
{
path: ':id',
component: UserDetailComponent,
resolve: { user: userResolver }
}
]
}
]
}
];
// ============================================
// USAGE
// ============================================
// Navigate to /admin/users
// AdminComponent renders in root outlet
// UsersComponent renders in AdminComponent's outlet
// Users list displayed
// Click a user → /admin/users/42
// AdminComponent stays mounted
// UsersComponent stays mounted (route unchanged)
// UserDetailComponent renders in UsersComponent's outlet
What this shows:
- AdminComponent — shared shell with sidebar and main content
- UsersComponent — list of users, plus its own outlet for the detail
- UserDetailComponent — renders in the child outlet when a user is selected
- Resolvers —
usersResolverat the list level,userResolverat the detail - Relative links —
routerLink="users"resolves to/admin/users - Redirects —
''redirects toadmin;admin‘s empty child redirects tousers
The URL structure mirrors the component structure. Each level renders in its parent’s outlet.
Why this shape: It’s a real admin dashboard. The sidebar stays, the list stays while a detail is open, and only the detail changes. Resolvers load the data at the right level. Adding a new section means adding a route under
admin.
Complete Example Session
# ============================================
# PART 1: BASIC NESTED ROUTES
# ============================================
cat > routes.ts << 'EOF'
import { Routes } from '@angular/router';
import { SettingsComponent } from './settings.component';
import { ProfileComponent } from './profile.component';
import { AccountComponent } from './account.component';
export const routes: Routes = [
{
path: 'settings',
component: SettingsComponent,
children: [
{ path: '', component: ProfileComponent },
{ path: 'profile', component: ProfileComponent },
{ path: 'account', component: AccountComponent }
]
}
];
EOF
# ============================================
# PART 2: PARENT COMPONENT
# ============================================
cat > settings.component.ts << 'EOF'
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-settings',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<h1>Settings</h1>
<nav>
<a routerLink="profile" routerLinkActive="active">Profile</a>
<a routerLink="account" routerLinkActive="active">Account</a>
</nav>
<router-outlet></router-outlet>
`
})
export class SettingsComponent {}
EOF
npx tsc --noEmit settings.component.ts
# (no errors)
# ============================================
# PART 3: CHILD COMPONENTS
# ============================================
cat > profile.component.ts << 'EOF'
import { Component } from '@angular/core';
@Component({
selector: 'app-profile',
standalone: true,
template: `<p>Profile settings</p>`
})
export class ProfileComponent {}
EOF
cat > account.component.ts << 'EOF'
import { Component } from '@angular/core';
@Component({
selector: 'app-account',
standalone: true,
template: `<p>Account settings</p>`
})
export class AccountComponent {}
EOF
npx tsc --noEmit profile.component.ts account.component.ts
# (no errors)
# ============================================
# PART 4: DEEP NESTING
# ============================================
cat > deep.ts << 'EOF'
import { Routes } from '@angular/router';
const routes: Routes = [
{
path: 'admin',
children: [
{
path: 'users',
children: [
{
path: ':id',
children: [
{ path: 'edit', component: null as any }
]
}
]
}
]
}
];
export { routes };
EOF
npx tsc --noEmit deep.ts
# (no errors)
# ============================================
# PART 5: RELATIVE NAVIGATION
# ============================================
cat > nav.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-nav',
standalone: true,
template: `
<button (click)="goUp()">Up</button>
<button (click)="goToAccount()">Account</button>
`
})
export class NavComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
goUp(): void {
this.router.navigate(['..'], { relativeTo: this.route });
}
goToAccount(): void {
this.router.navigate(['../account'], { relativeTo: this.route });
}
}
EOF
npx tsc --noEmit nav.ts
# (no errors)
# ============================================
# PART 6: NESTED OUTLETS
# ============================================
cat > outlets.ts << 'EOF'
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-parent',
standalone: true,
imports: [RouterOutlet],
template: `
<div class="parent">
<router-outlet></router-outlet>
</div>
`
})
export class ParentComponent {}
@Component({
selector: 'app-child',
standalone: true,
imports: [RouterOutlet],
template: `
<div class="child">
<router-outlet></router-outlet>
</div>
`
})
export class ChildComponent {}
EOF
npx tsc --noEmit outlets.ts
# (no errors)
# ============================================
# PART 7: REDIRECTS
# ============================================
cat > redirect.ts << 'EOF'
import { Routes } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: 'admin', pathMatch: 'full' },
{
path: 'admin',
children: [
{ path: '', redirectTo: 'users', pathMatch: 'full' },
{ path: 'users', children: [] }
]
}
];
export { routes };
EOF
npx tsc --noEmit redirect.ts
# (no errors)
# ============================================
# PART 8: SUMMARY
# ============================================
cat << 'EOF'
Nested route structure:
{
path: 'parent',
component: ParentComponent,
children: [
{ path: '', component: DefaultChild },
{ path: 'a', component: AComponent },
{ path: 'b', component: BComponent }
]
}
URLs:
/parent → ParentComponent > DefaultChild
/parent/a → ParentComponent > AComponent
/parent/b → ParentComponent > BComponent
Templates:
AppComponent → <router-outlet>
ParentComponent → <router-outlet> ← child renders here
Reading params:
route.paramMap → this level's params
route.parent.paramMap → parent's params
route.data → merged data from ancestors
Relative navigation:
routerLink="a" → /parent/a
routerLink="../b" → /parent/b (from /parent/a)
navigate(['..'], { relativeTo: this.route })
EOF
Quick Reference
Child Route Syntax
| Form | Meaning |
|---|---|
children: [...] | Array of child routes |
path: '' | Matches parent’s URL exactly |
path: '**' | Wildcard within parent’s prefix |
Nesting Depth
| Level | Example | Outlets |
|---|---|---|
| Root | / | App outlet |
| Level 1 | /parent | Parent outlet |
| Level 2 | /parent/child | Child outlet |
| Level N | /a/b/c/... | One per level |
Template Requirements
| Component | Needs |
|---|---|
| Parent | <router-outlet> |
| Child | (its own template) |
| Child with grandchildren | <router-outlet> |
Relative Navigation
| From | Link | Result |
|---|---|---|
/parent | routerLink="child" | /parent/child |
/parent/a | routerLink="../b" | /parent/b |
/parent/a/b | routerLink="../../c" | /parent/c |
/parent | navigate(['child']) + relativeTo | /parent/child |
ActivatedRoute Navigation
| Property | Provides |
|---|---|
route.snapshot | This level’s snapshot |
route.parent | Parent route |
route.children | Child routes |
route.firstChild | First child |
route.pathFromRoot | Full chain |
Data Inheritance
| Level | Data |
|---|---|
| Parent | Its data |
| Child | Parent’s data merged with its own |
| Grandchild | All ancestors merged |
Redirect in Children
| Config | Effect |
|---|---|
{ path: '', redirectTo: 'x', pathMatch: 'full' } | Parent URL → child x |
{ path: '**', redirectTo: 'default' } | Unknown child → default |
Named Outlets
| Form | Purpose |
|---|---|
<router-outlet name="x"> | Named outlet |
outlet: 'x' in route | Target named outlet |
/(x:path) | URL syntax |
Common Patterns
| Pattern | Structure |
|---|---|
| Sidebar + content | Parent sidebar, child content |
| Tabs | Parent tabs, child tab content |
| Master-detail | Parent list, child detail |
| Wizard | Parent shell, child steps |
Error Cases
| Error | Cause |
|---|---|
| Child doesn’t render | Missing <router-outlet> in parent |
| Wrong relative link | Mismatched ../ count |
| Parent’s params read as child’s | Wrong route level |
| Route doesn’t match | Wrong nesting |
Route Data Flow
| Source | Access |
|---|---|
| This route’s data | route.data |
| Parent’s data | route.parent.data |
| All ancestors | Walk parent chain |
| Merged in snapshot | route.snapshot.data |
pathMatch in Children
| Config | Meaning |
|---|---|
path: '', pathMatch: 'full' | Exact parent URL only |
path: '', pathMatch: 'prefix' | (default) prefix match |
Best Practices
✅ Do This:
// Use children for hierarchical routes
{
path: 'settings',
component: SettingsComponent,
children: [
{ path: 'profile', component: ProfileComponent }
]
} // ✅
// Include a router-outlet in the parent
template: `<router-outlet></router-outlet>` // ✅
// Provide a default child with `path: ''`
{ path: '', component: DefaultChild } // ✅
// Use relative links in the parent
<a routerLink="profile">Profile</a> // ✅
// Use `relativeTo` for programmatic relative navigation
this.router.navigate(['../a'], { relativeTo: this.route }); // ✅
// Resolve data at the level it's needed
{ path: 'users', resolve: { users: usersResolver }, children: [...] } // ✅
// Read parent data via route.parent
const users = this.route.parent?.snapshot.data['users']; // ✅
// Use redirects for convenience
{ path: '', redirectTo: 'users', pathMatch: 'full' } // ✅
// Name outlets when you need more than one per level
<router-outlet></router-outlet>
<router-outlet name="sidebar"></router-outlet> // ✅
❌ Don’t Do This:
// Don't forget the router-outlet in the parent
template: `<h1>Settings</h1>` // ⚠️ child never renders // ⚠️
// Don't use absolute links when relative is shorter
<a routerLink="/settings/profile">Profile</a> // ⚠️ breaks if parent changes // ⚠️
// Don't flatten when nesting is natural
{
path: 'settings/profile', component: P
},
{
path: 'settings/account', component: A
}
// ⚠️ duplicates the layout // ⚠️
// Don't forget relativeTo in code
this.router.navigate(['edit']); // ⚠️ goes to /edit // ⚠️
// Don't read parent params without walking the chain
const parentId = this.route.paramMap.get('parentId'); // ⚠️ not here // ⚠️
// Don't over-nest deeply
// 5+ levels get confusing // ⚠️
// Don't forget default children for parent URLs
// /settings would render nothing without path: '' // ⚠️
// Don't mix absolute and relative navigation carelessly
// Understand the base // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Missing outlet in parent | Child doesn’t render | Add <router-outlet> |
| Absolute links break on rename | Hard to maintain | Use relative links |
| Parent URL renders nothing | No path: '' | Add default child |
Wrong ../ count | Wrong target | Count the levels |
| Parent params read as child’s | Wrong route level | Walk parent |
| Child route doesn’t match | Wrong path | Check nesting |
| Deep nesting | Confusing | Flatten or restructure |
| Redirect loops | '' → '' | Add pathMatch: 'full' |
| Named outlet mismatch | Wrong name | Match route to outlet |
| Data not inherited | Read wrong route | Use route.parent |
Real-World Examples
1. Settings with children
{
path: 'settings',
component: SettingsComponent,
children: [
{ path: 'profile', component: ProfileComponent },
{ path: 'account', component: AccountComponent }
]
}
2. Admin shell
{
path: 'admin',
component: AdminComponent,
children: [
{ path: 'users', component: UsersComponent },
{ path: 'settings', component: SettingsComponent }
]
}
3. Master-detail
{
path: 'users',
component: UsersComponent,
children: [
{ path: ':id', component: UserDetailComponent }
]
}
4. Default child
{ path: '', component: OverviewComponent }
5. Child redirect
{ path: '', redirectTo: 'overview', pathMatch: 'full' }
6. Child wildcard
{ path: '**', component: NotFoundComponent }
7. Relative link
<a routerLink="profile">Profile</a>
8. Relative up
<a routerLink="../account">Account</a>
9. Programmatic relative
this.router.navigate(['edit'], { relativeTo: this.route });
10. Parent data read
const users = this.route.parent?.snapshot.data['users'];
11. Resolver at parent
{
path: 'users',
resolve: { users: usersResolver },
children: [...]
}
12. Resolver at child
{
path: ':id',
component: UserDetailComponent,
resolve: { user: userResolver }
}
13. Named outlet
<router-outlet name="sidebar"></router-outlet>
14. Route to named outlet
{ path: 'chat', component: ChatComponent, outlet: 'sidebar' }
15. Multi-level nesting
{
path: 'a',
children: [
{
path: 'b',
children: [
{ path: 'c', component: CComponent }
]
}
]
}
16. Grandparent params
const grandparentId = this.route.parent?.parent?.paramMap.get('id');
17. Breadcrumb with data
{
path: 'users',
data: { breadcrumb: 'Users' },
children: [...]
}
18. Nested layout with tabs
{
path: 'account',
component: AccountShellComponent,
children: [
{ path: 'profile', component: ProfileComponent },
{ path: 'security', component: SecurityComponent }
]
}
19. Lazy-loading children
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.routes)
}
20. Empty parent with children
{
path: 'admin',
children: [
{ path: 'users', component: UsersComponent }
]
}
// No component — children render directly in parent's outlet
Visual: Nested Route Structure
┌──────────────────────────────────────────────┐
│ Routes: │
│ │
│ { │
│ path: 'settings', │
│ component: SettingsComponent, │
│ children: [ │
│ { path: '', component: Overview }, │
│ { path: 'profile', component: Profile }│
│ { path: 'account', component: Account }│
│ ] │
│ } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ URLs: │
│ │
│ /settings → Settings > Overview │
│ /settings/profile → Settings > Profile │
│ /settings/account → Settings > Account │
│ │
└──────────────────────────────────────────────┘
Visual: Rendering Tree
┌──────────────────────────────────────────────┐
│ <app-root> │
│ <nav>...</nav> │
│ <router-outlet> ← root outlet │
│ <app-settings> ← parent │
│ <h1>Settings</h1> │
│ <nav>...</nav> │
│ <router-outlet> ← child outlet │
│ <app-profile> ← child │
│ ... │
│ </app-profile> │
│ </router-outlet> │
│ </app-settings> │
│ </router-outlet> │
│ </app-root> │
│ │
└──────────────────────────────────────────────┘
Visual: URL to Components
┌──────────────────────────────────────────────┐
│ URL: /admin/users/42 │
│ │
│ Root outlet │
│ └── AdminComponent │
│ └── Child outlet │
│ └── UsersComponent │
│ └── Child outlet │
│ └── UserDetailComponent│
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ URL changes: /admin/users/43 │
│ │
│ AdminComponent (reused) │
│ UsersComponent (reused) │
│ UserDetailComponent (recreated) │
│ │
│ Only the deepest component changes │
│ │
└──────────────────────────────────────────────┘
Visual: Relative Navigation
┌──────────────────────────────────────────────┐
│ Current URL: /settings/profile │
│ │
│ routerLink="edit" │
│ → /settings/profile/edit │
│ │
│ routerLink="../account" │
│ → /settings/account │
│ │
│ routerLink="../../other" │
│ → /other │
│ │
└──────────────────────────────────────────────┘
Visual: Shared Layout
┌──────────────────────────────────────────────┐
│ Parent component (DashboardComponent) │
│ │
│ ┌────────────────┬─────────────────────┐ │
│ │ │ │ │
│ │ Sidebar │ <router-outlet> │ │
│ │ │ │ │
│ │ Overview │ ← child renders │ │
│ │ Analytics │ here │ │
│ │ Reports │ │ │
│ │ Settings │ │ │
│ │ │ │ │
│ └────────────────┴─────────────────────┘ │
│ │
│ Sidebar stays mounted │
│ Only the child in the outlet changes │
│ │
└──────────────────────────────────────────────┘
Visual: Activation Order
┌──────────────────────────────────────────────┐
│ Navigate to /admin/users/42 │
│ │ │
│ ▼ │
│ Root route guards │
│ │ │
│ ▼ │
│ AdminComponent created │
│ │ │
│ ▼ │
│ admin/users guards │
│ │ │
│ ▼ │
│ UsersComponent created │
│ │ │
│ ▼ │
│ admin/users/:id guards │
│ │ │
│ ▼ │
│ UserDetailComponent created │
│ │
│ Parent first, then children │
│ │
└──────────────────────────────────────────────┘
Visual: ActivatedRoute Chain
┌──────────────────────────────────────────────┐
│ UserDetailComponent's route: │
│ │
│ route │
│ ├── paramMap → id = '42' │
│ └── parent │
│ ├── paramMap → (none) │
│ └── parent │
│ ├── paramMap → (none) │
│ └── parent → null │
│ │
│ Walk parent for ancestor params │
│ │
└──────────────────────────────────────────────┘
Visual: Redirect Flow
┌──────────────────────────────────────────────┐
│ User visits /settings │
│ │ │
│ ▼ │
│ Parent route matches │
│ │ │
│ ▼ │
│ Child path '' has redirectTo 'profile' │
│ │ │
│ ▼ │
│ Router redirects to /settings/profile │
│ │ │
│ ▼ │
│ ProfileComponent renders │
│ │
└──────────────────────────────────────────────┘
Visual: When to Nest
┌──────────────────────────────────────────────┐
│ Shared layout? │
│ │ │
│ └── Yes ──► Nested routes │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Parent URL is meaningful? │
│ │ │
│ └── Yes ──► Nested routes │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Child state depends on parent state? │
│ │ │
│ └── Yes ──► Nested routes │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Unrelated pages? │
│ │ │
│ └── Flat routes │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Setting up routes? │
│ │ │
│ ├── Flat ──► { path, component } │
│ │ │
│ └── Hierarchical ──► { path, children }│
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Parent has a template? │
│ │ │
│ └── Add <router-outlet> │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Parent URL needs content? │
│ │ │
│ └── Add { path: '', component } child │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Links in parent? │
│ │ │
│ └── Use relative (no leading /) │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| Nested route | Route inside a children array |
| Parent outlet | Where the child renders |
| Child outlet | Where the grandchild renders |
| Relative link | Resolved against the current route |
relativeTo | Base for programmatic navigation |
route.parent | Access to the parent route |
| Data inheritance | Parent data flows to children |
| Resolver per level | Each level resolves its own |
| Empty child | path: '' for the parent’s URL |
Key takeaways:
- Nested routes render children inside a parent’s
<router-outlet> - The parent must have an outlet — without it, the child has nowhere to render
- URLs are hierarchical —
/parent/child— and the config mirrors the structure - Shared layout lives in the parent — sidebar, tabs, header — and stays mounted
- Only the child changes on navigation within the parent
- Use relative links (
routerLink="child") — they resolve against the current route - Use
relativeTo: this.routein programmatic navigation path: ''in children matches the parent’s URL exactly — use it for a default view- Wildcards in children match only under the parent’s prefix
- Resolvers and guards run per level — parent first, then child
- Data inherits — children see the parent’s
data - Read ancestor params by walking
route.parent - Redirects in children route the parent URL to a specific child
- Keep nesting shallow — a few levels is fine, ten is a smell
Remember: Nested routes model hierarchical pages. The parent provides the frame and stays mounted; children fill the outlet and swap on navigation. Each level has its own outlet, its own guards, its own resolvers, and its own ActivatedRoute. Use relative links so the parent can move without breaking children. Include path: '' for the parent’s default view. The result is a routing config that mirrors your pages — readable, composable, and efficient.
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!