We use cookies to enhance your experience on the site
CodeWorlds

Nested Routes and Resolve - Rooms Inside the Dojo

A dojo is not one hall - it is a building with rooms inside rooms: a training hall, a weapons corner within it, and inside that the profile of one particular samurai. Angular routes nest exactly the same way. In this lesson you will learn two things: how to build routes inside routes (a parent and its children) and how to use a resolver to fetch data before the screen ever appears.

Child Routes - Paths Inside Paths

A route can carry a

children
property - an array of routes that render inside the parent. There is no
nested
property in a route definition, there is no
RouterChildModule
to import, and you do not need a separate routes file for every level: one array, nested as deep as the building goes. Look at how
/dojo
holds the training hall, and how a samurai profile has tabs of its own.

1export const routes: Routes = [
2  {
3    path: 'dojo',
4    component: DojoLayoutComponent,
5    children: [
6      { path: '', component: DojoHomeComponent },
7      { path: 'training', component: TrainingComponent },
8      {
9        path: 'samurai/:id',
10        component: SamuraiProfileComponent,
11        children: [
12          { path: '', redirectTo: 'info', pathMatch: 'full' },
13          { path: 'info', component: SamuraiInfoComponent },
14          { path: 'skills', component: SamuraiSkillsComponent }
15        ]
16      }
17    ]
18  }
19];

Read it like a floor plan:

/dojo/training
is the training hall inside the dojo, and
/dojo/samurai/5/skills
is the skills tab in the profile of samurai number 5. A weapons corner would simply be one more entry in the same array. Notice that child paths are relative to the parent - you write
training
, not
/dojo/training
, and Angular joins the segments for you. Two more tricks: the empty path
{ path: '' }
is the parent's default view (what to show when nobody has picked a child yet), and
redirectTo: 'info'
sends anyone who lands on the bare profile straight to the info tab, so the screen is never blank. Read the parent entry top to bottom and you have its shape: the opening brace, then
path
, then
component
, then
children
, then the closing brace.

For the children to have somewhere to appear, the parent needs its own

router-outlet
:

1@Component({
2  template: `
3    <header>
4      <h1>Dojo</h1>
5      <nav><a routerLink="training">Training</a></nav>
6    </header>
7
8    <router-outlet></router-outlet>
9  `
10})
11export class DojoLayoutComponent { }

This is the heart of nesting:

DojoLayoutComponent
draws the shared frame (header, navigation) and
<router-outlet>
is the window where the active child appears - and it sits in the parent component's own template. Angular has no
<child-outlet>
element, and the children do not land directly in
app.component
- that top-level outlet is where
DojoLayoutComponent
itself was rendered. Go to
/dojo/training
and
TrainingComponent
fills the window while the dojo header stays exactly where it was. Every level of nesting has its own outlet, like a room inside a room.

Resolver - Data Before You Enter

By default the component loads at once and you pull the data in

ngOnInit
- so for a moment the screen sits empty. A resolver reverses that order: it fetches the data before the route activates, and the component has the full set from its very first frame.

1import { ResolveFn } from '@angular/router';
2
3export const samuraiResolver: ResolveFn<Samurai> = (route) => {
4  const service = inject(SamuraiService);
5  const id = route.paramMap.get('id')!;
6  return service.getById(+id);
7};
8
9// In the route definition
10{
11  path: 'samurai/:id',
12  component: SamuraiDetailComponent,
13  resolve: { samurai: samuraiResolver }
14}

A resolver is a function that returns the data for a route - here it fetches a samurai by the

id
read from the address. Its type is
ResolveFn<T>
, the modern functional form and the one to reach for in Angular 19; the old shape was a class implementing the
Resolve
interface, while names like
ResolverFn<T>
or
DataResolver
do not exist at all, so mind the spelling. Attaching it takes a single property, built from five pieces:
resolve:
opens an object
{
, inside it a key you choose -
samurai:
- then the function
samuraiResolver
, then the closing
}
. Despite the name, a resolver settles no conflicts between routes, validates no URL parameters and performs no conditional redirects -
redirectTo
and guards do that. Its one job is to fetch data before activating a route, which is why you never see a profile flash empty. In the component you collect the finished data straight from the route:

1export class SamuraiDetailComponent {
2  private route = inject(ActivatedRoute);
3  samurai = this.route.snapshot.data['samurai'] as Samurai;
4}

The key

samurai
in
data
is precisely the name you gave in
resolve: { samurai: ... }
- rename one and you must rename the other. Mind which bag you reach into as well:
snapshot.data
holds what the resolvers produced, while
snapshot.params
holds the raw values from the URL, so
params['samurai']
would come back empty, and
this.router.data
or
this.route.resolve
are not real properties at all. Instead of calling a service and juggling a loading state inside the component, you just read a ready-made object. The trade-off is deliberate: the user waits a heartbeat longer for the transition but never stares at an empty screen - ideal for pages that mean nothing without their data.

Remember two things from this lesson: routes nest through

children
and render in the parent's
router-outlet
, like rooms inside the dojo; and a resolver fetches the data before you enter, so the component never starts out empty.

Go to CodeWorlds