Skip to content

Ember Nested Routes — Hierarchical Layouts and Data

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Ember Nested Routes. We cover key concepts, practical examples, and best practices to help you master this topic.

Ember nested routes create hierarchical URL structures with shared parent layouts. Child routes render into the parent's {{outlet}}, enabling patterns like sidebar-main layouts, multi-step wizards, and sectioned admin panels with shared navigation.

What You'll Learn

You will learn how to nest routes, share layouts between routes, access parent model data from child routes, and build breadcrumb navigation.

Why It Matters

Nested routes eliminate redundant templates. Instead of repeating the sidebar on every settings page, the parent settings route renders it once. This keeps templates DRY and consistent.

Real-World Use

An admin panel uses nested routes: /admin/users, /admin/users/:id, /admin/users/:id/edit. The admin layout template provides the sidebar navigation once, and each child route renders only its unique content.

flowchart TD
    A[/admin] --> B[admin.hbs]
    B --> C[{{outlet}}]
    C --> D[/admin/users]
    C --> E[/admin/settings]
    C --> F[/admin/reports]
    D --> G[users.hbs]
    D --> H[{{outlet}}]
    H --> I[/admin/users/new]
    H --> J[/admin/users/:id]

Defining Nested Routes

Use a callback function to define children within this.route().

// app/router.js
Router.map(function() {
  this.route('admin', function() {
    this.route('users');
    this.route('users', function() {
      this.route('user', { path: ':user_id' });
      this.route('new');
    });
    this.route('settings');
    this.route('reports');
  });
});

Parent Route Template with Outlet

The parent template defines the shared layout and places {{outlet}} where child routes render.

{{! app/templates/admin.hbs }}
<div class="admin-layout">
  <aside class="admin-sidebar">
    <nav>
      <LinkTo @route="admin.users">Users</LinkTo>
      <LinkTo @route="admin.settings">Settings</LinkTo>
      <LinkTo @route="admin.reports">Reports</LinkTo>
    </nav>
  </aside>

  <main class="admin-content">
    {{outlet}}
  </main>
</div>

Child Route Handlers

Child route handlers live in nested folders matching the URL structure.

// app/routes/admin/users.js
import Route from '@ember/routing/route';

export default class AdminUsersRoute extends Route {
  async model() {
    return this.store.findAll('user');
  }
}
{{! app/templates/admin/users.hbs }}
<h1>Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    {{#each this.model as |user|}}
      <tr>
        <td>{{user.name}}</td>
        <td>{{user.email}}</td>
        <td>
          <LinkTo @route="admin.users.user" @model={{user.id}}>
            View
          </LinkTo>
        </td>
      </tr>
    {{/each}}
  </tbody>
</table>

<LinkTo @route="admin.users.new">Add User</LinkTo>
{{outlet}}

Accessing Parent Route Data

Child routes can access parent route data using modelFor.

// app/routes/admin/users/user.js
import Route from '@ember/routing/route';

export default class AdminUsersUserRoute extends Route {
  model(params) {
    // Access parent model
    let users = this.modelFor('admin.users');
    return users.findBy('id', params.user_id);
  }

  setupController(controller, model) {
    super.setupController(controller, model);
    // Access multiple parent levels
    let adminModel = this.modelFor('admin');
    controller.set('adminTitle', adminModel.title);
  }
}
{{! app/templates/admin/users/user.hbs }}
<h2>User: {{this.model.name}}</h2>

<div class="user-detail">
  <p><strong>Email:</strong> {{this.model.email}}</p>
  <p><strong>Role:</strong> {{this.model.role}}</p>
  <p><strong>Joined:</strong> {{this.model.createdAt}}</p>
</div>

<div class="user-actions">
  <LinkTo @route="admin.users">Back to Users</LinkTo>
</div>

Multiple Outlets

Use named outlets to render child routes into specific locations.

Router.map(function() {
  this.route('products', function() {
    this.route('product', { path: ':product_id' });
    this.route('compare');
  });
});
{{! app/templates/products.hbs }}
<div class="products-layout">
  <div class="products-list">
    {{outlet "sidebar"}}
  </div>
  <div class="products-main">
    {{outlet}}
  </div>
  <div class="products-compare">
    {{outlet "compare"}}
  </div>
</div>

Use nested route names to build breadcrumbs.

// app/controllers/admin/users/user.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default class AdminUsersUserController extends Controller {
  @service router;

  get breadcrumbs() {
    return [
      { label: 'Home', route: 'index' },
      { label: 'Admin', route: 'admin' },
      { label: 'Users', route: 'admin.users' },
      { label: this.model.name, route: 'admin.users.user' }
    ];
  }
}
{{! app/components/breadcrumbs.hbs }}
<nav class="breadcrumbs" aria-label="breadcrumb">
  <ol>
    {{#each @breadcrumbs as |crumb index|}}
      <li>
        {{#if (eq index (sub @breadcrumbs.length 1))}}
          <span aria-current="page">{{crumb.label}}</span>
        {{else}}
          <LinkTo @route={{crumb.route}}>{{crumb.label}}</LinkTo>
        {{/if}}
      </li>
    {{/each}}
  </ol>
</nav>

Loading and Error Substates

Nested routes support loading and error templates per nesting level.

{{! app/templates/admin/loading.hbs }}
{{! Shows while ANY child of admin is loading }}
<div class="admin-loading">
  <div class="spinner"></div>
  <p>Loading admin section...</p>
</div>
{{! app/templates/admin/error.hbs }}
{{! Shows when ANY child of admin has an error }}
<div class="admin-error">
  <h2>Admin Error</h2>
  <p>There was a problem loading this section.</p>
  <p>{{this.model.message}}</p>
  <LinkTo @route="admin">Back to Admin Home</LinkTo>
</div>

Common Mistakes

  1. Too many nesting levels. Three levels maximum. Deeper nesting makes code hard to follow and hurts performance.
  2. Forgetting {{outlet}} in every parent. Each nesting level needs its own {{outlet}}. Missing outlet means children never render.
  3. Accessing modelFor with wrong route name. Use the full route name as defined in router.js. Check for typos.
  4. Loading all data in the parent model hook. Each route should load only its own data. Parent loads shared data, child loads specific data.
  5. Not handling loading states per level. A single loading template covers all children. Create granular loading templates for better UX.

Practice Questions

  1. How do you define a nested route in Ember?
  2. What is the purpose of {{outlet}} in parent templates?
  3. How do you access parent model data from a child route?
  4. Why should you limit nesting depth?
  5. Challenge: Create a nested route structure for a documentation site: /docs, /docs/:section, /docs/:section/:page. Each level has its own layout. Breadcrumbs should show the full path. Use named outlets for sidebar and content.

FAQ

Can I nest routes more than 3 levels?

Technically yes, but it is not recommended. Deep nesting complicates data flow and performance.

{{< faq "How do I render a child route in a different outlet?" "Use named outlets: `{{outlet \"sidebar\"}}` and specify the outlet name in `transitionTo` or `LinkTo`." >}}
What is `modelFor` and when should I use it?

modelFor('parent.route') returns the model from a parent route handler. Use it when a child needs parent data.

Do nested routes share the same controller?

No. Each route has its own controller. Parent and child controllers are separate.

How do I reset state when navigating between nested routes?

Use resetController in the route to clean up state on exit.

Mini Project

Build a documentation browser with three levels of nested routes. Level 1: documentation sections list. Level 2: section overview with page list in sidebar. Level 3: individual page content. Each level has loading and error substates. Breadcrumbs at the top show the current path. Use modelFor in level 3 to show section title.

What's Next

Now that you understand nested routes, learn Ember Templates for Handlebars syntax. Then explore Ember Components for reusable UI.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro