Build an Angular Microfrontend Shell

Build a resilient Angular shell that loads independent remotes at runtime.

Introduction

30 Second Summary

A business portal can look healthy until one feature server disappears. When the entire experience collapses with it, separate deployments still share one fragile point of failure.

In this project, you will build a resilient Angular microfrontend shell with an independently served dashboard. You will use Native Federation plus a runtime manifest to compose both applications on your machine.

What You'll Build

You will demo an Operations Portal where selecting Dashboard loads remote-owned health cards beneath navigation that stays visible through remote failures.

By the end of this project, you'll have:

  • A shell-owned Home view with persistent navigation that remains available without the dashboard server.
  • An independently served dashboard that you can open alone or render inside the Operations Portal.
  • A resilient Dashboard route that discovers its remote from a deployment-time manifest. It shows a shell-owned recovery view when loading fails.
  • Secret Mission: Add a second independently served Reports remote to the same manifest.

Are there any prerequisites?

You should have advanced experience with Angular and TypeScript. The project validates your local Node.js toolchain before you create the workspace.

Before We Start

This step fixes the project goal in your mind before any hands-on work begins. Your decisions will stay focused on a runtime-composed Angular shell with a dashboard that represents a separately built feature application.

Scaffold the Angular Workspace

A multi-application Angular workspace gives the shell its own build target. It gives the dashboard a separate build target before runtime composition begins.

This step checks Node.js in Windows PowerShell before creating two standalone applications. Visual Studio Code will display the finished workspace.

In this step, get ready to:
  • Validate your Windows toolchain.
  • Create the angular-mfe-lab workspace with Angular CLI 22.2.2.
  • Generate independently runnable shell plus dashboard applications.
Validate the Windows toolchain

Angular 22 supports specific Node.js release lines. Checking your installed version now prevents compatibility failures during scaffolding.

  • Start Windows PowerShell from Windows Search.
  • Check Node.js plus npm by running these commands:
node --version
npm --version

How Do I Read the Versions?

  • Use Node.js 22.22.3 or later within major version 22.
  • Use Node.js 24.15.0 or later within major version 24.
  • Use Node.js 26.0.0 or later within major version 26.
  • Confirm that npm --version prints a version number.

✔️ I see a supported version

Your Node.js release falls inside Angular 22's supported range. Your npm command also returned a version number.

ⓧ I see an unsupported version

  • Visit the official Node.js download page.
  • Install a release from one of the supported version lines listed above.
  • Restart Windows PowerShell from Windows Search.
  • Run the same two version checks again.

ⓧ Node.js is unavailable

  • Visit the official Node.js download page.
  • Install a release from one of the supported version lines listed above.
  • Restart Windows PowerShell from Windows Search.
  • Run the same two version checks again.
Install the pinned CLI and create the workspace

A global Angular CLI installation makes the ng command available from PowerShell. Pinning it keeps the generated workspace aligned with this project.

The global install can take one or two minutes while npm resolves packages. A short pause during this command is expected.

  • Install Angular CLI 22.2.2 globally by running this command:
npm install --global @angular/cli@22.2.2

What Does This Command Do?

  • The --global flag makes the Angular CLI command available outside one project folder.
  • The package specification pins Angular CLI to 22.2.2.

PowerShell returns to its prompt when the installation finishes successfully.

Angular CLI Installation Failed?

Confirm that your Node.js version matches a supported line. Check that your internet connection can reach the npm registry.

Ask for help with the installation output: help me diagnose why the pinned Angular CLI installation failed in Windows PowerShell

  • Move to your Desktop by running this command:
cd ~/Desktop

Why Start on the Desktop?

The cd alias changes PowerShell's current location. The ~ character expands to your home folder.

Starting here places angular-mfe-lab somewhere easy to find from Visual Studio Code.

The workspace command installs npm dependencies after creating the files. This can take a few minutes while packages download.

  • Choose your preference if Angular CLI asks about sharing usage data.
  • Create the empty workspace by running this command:
ng new angular-mfe-lab --no-create-application --package-manager npm --skip-git

What Do the Workspace Flags Do?

  • The --no-create-application flag creates an empty workspace for multiple applications.
  • The --package-manager npm option installs workspace dependencies with npm.
  • The --skip-git flag leaves the workspace without a Git repository.

That is the workspace foundation in place. The angular-mfe-lab folder now contains an empty Angular CLI 22.2.2 workspace with npm dependencies.

Workspace Creation Stopped?

Check the final lines in PowerShell for a package download problem. Remove any incomplete angular-mfe-lab folder before running the workspace command again.

Ask for help with the scaffold output: help me diagnose why Angular CLI could not create my empty workspace

  • Move into the new angular-mfe-lab folder by running this command:
Set-Location -Path angular-mfe-lab -PassThru

What Does This Location Command Show?

The command changes PowerShell's current location to angular-mfe-lab. The -PassThru flag prints the resolved path as confirmation.

You should see a path ending in angular-mfe-lab.

  • Start Visual Studio Code from Windows Search.
  • Use the folder-opening option on the Visual Studio Code start screen.
  • Double-click angular-mfe-lab inside the Desktop folder.
  • Confirm that you trust the folder if Visual Studio Code asks.

You should see angular-mfe-lab as the open folder in Visual Studio Code. This confirms that your editor can access the local workspace.

Generate and run two standalone applications

Each generated application receives its own build target. Routing plus CSS support gives both applications the same starting capabilities before federation connects them.

  • Generate the shell plus dashboard applications by running these commands:
ng generate application shell --routing --style css --skip-tests
ng generate application dashboard --routing --style css --skip-tests

What Do the Application Flags Create?

  • The application generator creates standalone applications under projects.
  • The --routing flag adds route configuration to each application.
  • The --style css option selects CSS styling.
  • The --skip-tests flag omits generated test files.
  • Switch back to Visual Studio Code.
  • Expand projects in the file sidebar.

You should see separate projects/shell plus projects/dashboard folders. Each folder contains a generated routed Angular application with CSS styling.

Missing an Application Folder?

Confirm that PowerShell still shows a path ending in angular-mfe-lab. Run the missing generator command again from that folder.

Ask for help with the generated structure: help me find why one Angular application folder was not generated

An Angular development server keeps control of its terminal while it watches for file changes. Each application needs a separate PowerShell window.

Before you run the first server, do you expect the shell command to start the dashboard too?

  • Start the shell development server by running this command:
ng serve shell

What Does the Shell Server Prove?

Angular CLI builds only the shell target. The running process prints a local URL while it watches the shell files.

  • Open the printed local URL in your browser.

You should see the generated shell starter page at the printed URL.

Shell Starter Page Missing?

Keep the shell PowerShell window open because closing it stops the server. Check that you opened the exact local URL printed by that terminal.

Ask for help with the shell server: help me diagnose why my generated Angular shell is not loading

  • Start a second Windows PowerShell window from Windows Search.
  • Move the second terminal into angular-mfe-lab by running this command:
Set-Location -Path ~/Desktop/angular-mfe-lab -PassThru

Why Use a Second Terminal?

The shell server stays active in the first terminal. The second terminal can run the dashboard process from the same workspace.

You should see a path ending in angular-mfe-lab.

Before you start the dashboard, will it need its own local URL while the shell remains active?

  • Accept the suggested alternate port if Angular CLI reports that the default port is occupied.
  • Start the dashboard development server by running this command:
ng serve dashboard

What Does the Dashboard Server Prove?

Angular CLI builds only the dashboard target. Its printed local URL belongs to a separate development server.

  • Open the dashboard terminal's printed local URL in your browser.

You should see the generated dashboard starter page at its printed local URL.

Dashboard Server Did Not Start?

Confirm that the shell server still occupies the first terminal. Accept an alternate port when the dashboard server asks for one.

Ask for help with the second server: help me run two Angular applications from one workspace

Before you compare the two browser tabs, do you expect either starter application to depend on the other yet?

  • Return to the shell page at the URL printed by the first terminal.
  • Return to the dashboard page at the URL printed by the second terminal.

Each printed URL shows its own Angular starter application. That proves the shell plus dashboard are independently available before federation introduces runtime coupling.

You have the two-application foundation working. Each build target now runs from its own development server.

Your workspace is ready. Next, you'll replace the shell starter page with persistent navigation plus a locally owned home route.

Build the Visible Shell

Both generated Angular applications now run independently from the same workspace. That proves each application has its own build target.

The shell still shows starter content. It needs a permanent frame that survives every routed view.

In this step, you will turn the shell into the Operations Portal. Its header stays outside the RouterOutlet while the local Home route renders inside it.

In this step, get ready to:
  • Replace the starter shell with a persistent Operations Portal frame.
  • Add a shell-owned Home component.
  • Prove the local /home route works inside the persistent frame.
Replace the root shell component

The root component owns the parts of the interface that must remain available during navigation. The routed page appears inside its outlet.

  • Select projects/shell/src/app/app.ts from the file sidebar in Visual Studio Code.
  • Select all of the generated code in app.ts.
  • Replace the selected code by pasting this component:
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterLink, RouterLinkActive, RouterOutlet],
  template: `
    <header class="shell-header">
      <div>
        <p class="eyebrow">Runtime-composed workspace</p>
        <h1>Operations Portal</h1>
      </div>

      <nav aria-label="Primary navigation">
        <a routerLink="/home" routerLinkActive="active">Home</a>
        <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
      </nav>
    </header>

    <main class="shell-content">
      <router-outlet />
    </main>
  `,
})
export class App {}

What does this component own?

  • The App component imports the three Angular Router features used by its template.
  • The header contains the Operations Portal title plus its navigation links.
  • The router-outlet marks the area where Angular renders the active route.
  • The header sits outside the outlet. Routed content can change without removing the shell frame.
  • Save projects/shell/src/app/app.ts.
  • Return to the browser tab that displayed the shell earlier.
  • Refresh the shell page.

You should see the Operations Portal heading with Home and Dashboard links. The content area is empty until you connect its local route.

Still seeing the starter page?

Confirm that you edited the shell application at projects/shell/src/app/app.ts. Check that the file is saved before refreshing the browser.

If the shell reports a template error, compare every opening tag with its closing tag. Help me find the template error in my shell component.

The shell structure now has a visible boundary. Styling makes that ownership clear before routed features arrive.

  • In app.ts, find the closing backtick after </main>.
  • Add this styles property below the template property:
  styles: `
    :host {
      min-height: 100vh;
      display: block;
      background: #f4f7fb;
      color: #172033;
      font-family: Inter, system-ui, sans-serif;
    }
  `,

What does the host style control?

The :host selector styles the shell component itself. It gives the application a full-height canvas with its own background plus typography.

  • Save projects/shell/src/app/app.ts.
  • Refresh the shell page in your browser.

You should see the page background change to pale blue-gray. The shell text should use the new dark foreground colour.

Background unchanged?

Check that the styles property sits inside @Component. Confirm that its closing backtick appears before the closing component brace.

Ask for help with the inline style placement.

  • Place your cursor before the closing backtick of the styles property.
  • Add the header layout plus its title styling by pasting this CSS:
    .shell-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 2rem;
      padding: 1.5rem 2rem;
      background: #111827;
      color: #ffffff;
    }

    h1,
    .eyebrow {
      margin: 0;
    }

    .eyebrow {
      margin-bottom: 0.35rem;
      color: #93c5fd;
      font-size: 0.78rem;
      font-weight: 700;
      letter-spacing: 0.1em;
      text-transform: uppercase;
    }

How does the header take shape?

The header uses a horizontal flex layout to separate the title from navigation. Its dark background makes the shell boundary visible.

The eyebrow style establishes a smaller label above the Operations Portal title. Its uppercase treatment distinguishes context from the page heading.

  • Save projects/shell/src/app/app.ts.
  • Refresh the shell page in your browser.

You should see a dark header spanning the page. Runtime-composed workspace should appear above the Operations Portal title.

Header still unstyled?

Confirm that .shell-header matches class="shell-header" in the template. Check that the CSS remains inside the existing style backticks.

Help me compare the header class with its CSS selector.

  • Keep your cursor inside the styles property.
  • Add the navigation styling by pasting this CSS below the eyebrow rules:
    nav {
      display: flex;
      gap: 0.75rem;
    }

    nav a {
      padding: 0.65rem 0.9rem;
      border-radius: 0.6rem;
      color: #dbeafe;
      text-decoration: none;
    }

    nav a:hover,
    nav a.active {
      background: #2563eb;
      color: #ffffff;
    }

How does active navigation work?

The navigation rules arrange the links with consistent spacing. RouterLinkActive adds the active class when a link matches the current route.

The shared hover plus active rule gives the selected destination a blue background. That visual state makes the current route easy to identify.

  • Save projects/shell/src/app/app.ts.
  • Move your pointer over the Home link in the browser.

You should see the Home link gain a blue background. The Dashboard link should use the same hover treatment.

Links do not react?

Confirm that the selector reads nav a:hover. Check that both navigation items use anchor elements.

Help me debug the navigation styles.

  • Keep your cursor inside the styles property.
  • Finish the shell layout by pasting these rules below the navigation styles:
    .shell-content {
      max-width: 72rem;
      margin: 0 auto;
      padding: 2rem;
    }

    @media (max-width: 44rem) {
      .shell-header {
        align-items: flex-start;
        flex-direction: column;
      }
    }

How does the shell adapt?

The content area stays centred within a maximum width. Its padding keeps routed views away from the browser edge.

The media query stacks the header on narrower screens. The navigation remains part of the shell at every supported width.

  • Save projects/shell/src/app/app.ts.
  • Refresh the shell page in your browser.

You should see a centred content area below the header. Narrowing the browser should move the navigation below the title.

Layout rules missing?

Confirm that .shell-content matches the class on the main element. Check that the media query closes before the style backtick.

Help me inspect the shell layout styles.

✔️ Awesome, I've got everything!

Great. Save projects/shell/src/app/app.ts before moving to the Home view.

ⓧ I'd like to double check the full code

import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterLink, RouterLinkActive, RouterOutlet],
  template: `
    <header class="shell-header">
      <div>
        <p class="eyebrow">Runtime-composed workspace</p>
        <h1>Operations Portal</h1>
      </div>

      <nav aria-label="Primary navigation">
        <a routerLink="/home" routerLinkActive="active">Home</a>
        <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
      </nav>
    </header>

    <main class="shell-content">
      <router-outlet />
    </main>
  `,
  styles: `
    :host {
      min-height: 100vh;
      display: block;
      background: #f4f7fb;
      color: #172033;
      font-family: Inter, system-ui, sans-serif;
    }

    .shell-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 2rem;
      padding: 1.5rem 2rem;
      background: #111827;
      color: #ffffff;
    }

    h1,
    .eyebrow {
      margin: 0;
    }

    .eyebrow {
      margin-bottom: 0.35rem;
      color: #93c5fd;
      font-size: 0.78rem;
      font-weight: 700;
      letter-spacing: 0.1em;
      text-transform: uppercase;
    }

    nav {
      display: flex;
      gap: 0.75rem;
    }

    nav a {
      padding: 0.65rem 0.9rem;
      border-radius: 0.6rem;
      color: #dbeafe;
      text-decoration: none;
    }

    nav a:hover,
    nav a.active {
      background: #2563eb;
      color: #ffffff;
    }

    .shell-content {
      max-width: 72rem;
      margin: 0 auto;
      padding: 2rem;
    }

    @media (max-width: 44rem) {
      .shell-header {
        align-items: flex-start;
        flex-direction: column;
      }
    }
  `,
})
export class App {}

What should match?

This complete file keeps the header plus navigation outside router-outlet. The inline styles make that persistent shell boundary visible.

Add the local Home view

A shell-owned route proves the application remains useful without a feature server. Its message defines the shell's responsibility for navigation plus recovery.

  • Select the projects/shell/src/app folder in the Visual Studio Code file sidebar.
  • Create a file named home.ts inside that folder.
  • Paste this standalone component into home.ts:
import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  template: `
    <section class="panel">
      <p class="label">Shell-owned route</p>
      <h2>One frame, independently shipped features</h2>
      <p>
        Navigation, layout, and failure recovery stay available even when a
        feature remote cannot load.
      </p>
    </section>
  `,
})
export class Home {}

What does the Home component prove?

The Home component is a local standalone component owned by the shell. It does not depend on the dashboard application.

Its message identifies the capabilities that remain available around a failed feature remote. That ownership becomes the basis for resilience later.

  • Save projects/shell/src/app/home.ts.
  • Confirm that home.ts appears inside the projects/shell/src/app folder in the file sidebar.

The Home component now exists as a shell-owned file. The route configuration connects it to the browser.

Home file in the wrong folder?

Check that home.ts sits beside app.ts plus app.routes.ts. Move it into projects/shell/src/app if it appears elsewhere.

Help me confirm the Home component location.

Configure the local route

The router maps browser paths to components. An empty-path redirect gives the shell a predictable starting page.

  • Select projects/shell/src/app/app.routes.ts from the file sidebar.
  • Replace the file contents by pasting this route configuration:
import { Routes } from '@angular/router';
import { Home } from './home';

export const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'home',
  },
  {
    path: 'home',
    component: Home,
  },
];

How do these routes work?

  • The empty path redirects a visit to the shell toward /home.
  • The pathMatch value limits that redirect to the exact empty path.
  • The home route renders the shell-owned Home component inside the outlet.
  • Save projects/shell/src/app/app.routes.ts.

Before you start the shell, which parts do you expect to remain visible when the Home component fills the routed area?

  • Return to the PowerShell terminal for the shell application.
  • Start the shell development server by running this command:
ng serve shell

What does this command do?

The command builds the shell application plus starts its local development server. It watches the shell files for saved changes.

The server keeps this PowerShell terminal occupied while it runs. Use another terminal for future commands.

  • Open the local URL printed by the shell development server.
  • Wait for the shell page to finish loading.

You should see the Operations Portal header above the Home message. The Home link should have a blue active state.

Home route not rendering?

Confirm that app.routes.ts imports Home from ./home. Check that the terminal is serving the shell application from the angular-mfe-lab workspace.

Help me debug the local Home route.

✔️ Awesome, I've got everything!

Your empty path now redirects to the shell-owned Home component.

ⓧ I'd like to double check the full code

import { Routes } from '@angular/router';
import { Home } from './home';

export const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'home',
  },
  {
    path: 'home',
    component: Home,
  },
];

What should match?

This file contains one exact redirect plus one local route. Both routes stay entirely inside the shell application.

The route now works. The final styles turn its message into a distinct shell-owned panel.

  • Return to projects/shell/src/app/home.ts in Visual Studio Code.
  • Find the closing backtick after </section>.
  • Add this styles property below the template property:
  styles: `
    .panel {
      padding: 2rem;
      border: 1px solid #dbe3ef;
      border-radius: 1rem;
      background: #ffffff;
      box-shadow: 0 1rem 2.5rem rgb(15 23 42 / 8%);
    }
  `,

What defines the Home panel?

The .panel rule gives the local route a white surface with spacing plus a soft border. The shadow separates routed content from the shell background.

  • Save projects/shell/src/app/home.ts.
  • Return to the running shell in your browser.

You should see the Home message inside a rounded white panel. The Operations Portal header should remain unchanged above it.

Home panel still plain?

Confirm that the template uses class="panel". Check that the .panel rule sits inside the component's style backticks.

Help me connect the panel class to its style.

  • Place your cursor before the closing backtick of the styles property.
  • Finish the Home typography by pasting these rules below .panel:
    .label {
      color: #2563eb;
      font-size: 0.78rem;
      font-weight: 800;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    h2 {
      margin: 0.5rem 0;
      font-size: clamp(1.8rem, 4vw, 3rem);
    }

    p {
      max-width: 44rem;
      line-height: 1.65;
    }

How does the message gain hierarchy?

The label identifies the page as a shell-owned route. The larger heading communicates the deployment boundary at a glance.

The paragraph width keeps the resilience message readable on wide screens. Its line height adds space between wrapped lines.

  • Save projects/shell/src/app/home.ts.
  • Return to the running shell in your browser.

You should see a blue Shell-owned route label above a large heading. The resilience message should remain inside the white panel.

Typography unchanged?

Confirm that the label uses class="label". Check that each CSS rule closes before the component's style backtick.

Help me inspect the Home typography rules.

✔️ Awesome, I've got everything!

The shell-owned Home component is complete. Save projects/shell/src/app/home.ts before the final check.

ⓧ I'd like to double check the full code

import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  template: `
    <section class="panel">
      <p class="label">Shell-owned route</p>
      <h2>One frame, independently shipped features</h2>
      <p>
        Navigation, layout, and failure recovery stay available even when a
        feature remote cannot load.
      </p>
    </section>
  `,
  styles: `
    .panel {
      padding: 2rem;
      border: 1px solid #dbe3ef;
      border-radius: 1rem;
      background: #ffffff;
      box-shadow: 0 1rem 2.5rem rgb(15 23 42 / 8%);
    }

    .label {
      color: #2563eb;
      font-size: 0.78rem;
      font-weight: 800;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    h2 {
      margin: 0.5rem 0;
      font-size: clamp(1.8rem, 4vw, 3rem);
    }

    p {
      max-width: 44rem;
      line-height: 1.65;
    }
  `,
})
export class Home {}

What should match?

This complete component contains the local Home message plus its panel styling. It has no dependency on the dashboard application.

Before the final check, do you expect navigation to /home to replace the header or only the content below it?

  • Refresh the running shell in your browser.
  • Select the Home navigation link.
  • Confirm that the browser path ends with /home.

You should see the Operations Portal header plus its navigation above the shell-owned welcome panel. The Home link should remain active while the route content appears inside the outlet.

What did this prove?

The shell owns its header plus routing frame. The Home component owns only the content rendered inside RouterOutlet.

That separation preserves navigation when a future remote route cannot load. The shell already has a useful local destination.

Your shell now owns a stable frame plus a working local route. Next, you will give the independent dashboard a runtime contract.

Turn the Dashboard into a Remote

The Angular dashboard already runs independently. The shell still has no runtime contract for loading its feature view.

In this step, you will turn the dashboard into a Native Federation remote application. The dashboard keeps its standalone page while publishing a component that the shell can discover later.

Why Native Federation?

Native Federation implements runtime composition with ES modules plus import maps. It delegates builds to Angular's esbuild-based Application Builder.

Webpack Module Federation requires webpack 5 in both applications. Native Federation keeps this workspace aligned with Angular's current build system.

In this step, get ready to:
  • Build the remote-owned dashboard view.
  • Initialize the dashboard as a Native Federation remote.
  • Publish the dashboard component contract.
Build the remote-owned dashboard view

A remote should own a complete feature view. This dashboard owns its operational metrics plus their presentation.

  • Expand projects/dashboard/src/app in the Visual Studio Code Explorer sidebar.
  • Select the app folder.
  • Create dashboard-page.ts using the new-file control.
  • Add the first visible dashboard content by pasting this code into dashboard-page.ts:
import { Component } from '@angular/core';
@Component({
  selector: 'app-dashboard-page',
  template: `
    <section>
      <p class="label">Dashboard remote</p>
      <h2>Platform health</h2>
      <p class="summary">
        This view is built and served independently, then loaded into the shell
        at runtime.
      </p>
    </section>
  `,
})
export class DashboardPage {}

What does this component do?

  • The app-dashboard-page selector gives the dashboard entry component a way to render this view.
  • The summary makes the dashboard's independent deployment boundary visible.
  • The DashboardPage class becomes the component published by the remote.
  • Save dashboard-page.ts.
  • Select projects/dashboard/src/app/app.ts in the Explorer sidebar.
  • Replace the generated component with this dashboard entry component:
import { Component } from '@angular/core';
import { DashboardPage } from './dashboard-page';

@Component({
  selector: 'app-root',
  imports: [DashboardPage],
  template: `<app-dashboard-page />`,
})
export class App {}

What does this code do?

  • The imports metadata makes DashboardPage available to the root component template.
  • The app-dashboard-page element renders the remote-owned view when the dashboard runs independently.
  • The App class remains the dashboard application's bootstrap target.
  • Save app.ts.

✔️ Awesome, I've got everything!

Your dashboard entry component now renders DashboardPage directly. Make sure app.ts is saved before continuing.

ⓧ I'd like to double check the full code

import { Component } from '@angular/core';
import { DashboardPage } from './dashboard-page';

@Component({
  selector: 'app-root',
  imports: [DashboardPage],
  template: `<app-dashboard-page />`,
})
export class App {}

What should match?

Compare the import plus component metadata with your saved file. The template should contain only the app-dashboard-page element.

  • Switch back to a PowerShell terminal in the angular-mfe-lab workspace.
  • Start the standalone dashboard by running this command:
ng serve dashboard

What does this command do?

The Angular CLI builds the dashboard application. The development server watches saved dashboard files while it occupies this PowerShell terminal.

  • Open the local URL printed in the PowerShell output.

You should see the Dashboard remote label above the Platform health heading. The independent-runtime summary should appear underneath.

Dashboard view missing?

Check that app.ts imports DashboardPage from ./dashboard-page. Confirm that both files are saved.

Help me debug why DashboardPage is not rendering.

The page now proves that the dashboard owns a visible feature view. Next, its template needs the operational metrics that the remote supplies.

  • Return to projects/dashboard/src/app/dashboard-page.ts in the Explorer sidebar.
  • Find the closing </section> tag.
  • Insert the metric cards immediately above that tag using this code:
      <div class="metrics">
        <article>
          <span>API availability</span>
          <strong>99.98%</strong>
        </article>
        <article>
          <span>Open incidents</span>
          <strong>2</strong>
        </article>
        <article>
          <span>Deployments today</span>
          <strong>14</strong>
        </article>
      </div>

What do these cards show?

  • The metrics container groups the remote's operational cards.
  • Each article pairs one metric label with its value.
  • The values make the remote easy to recognize inside its standalone page later.
  • Save dashboard-page.ts.
  • Return to the dashboard browser tab.

The development server reloads the page. You should see API availability at 99.98%. You should also see 2 open incidents plus 14 deployments today.

Metric values missing?

Confirm that the metrics block sits inside the section element. Check that every article has a closing tag.

Help me repair the dashboard metric markup.

The metric content is working. The component's first style group turns that raw content into a distinct dashboard panel.

  • Find the closing backtick for the template property.
  • Insert this styles property below the template property:
  styles: `
    section {
      padding: 2rem;
      border-radius: 1rem;
      background: #ffffff;
      box-shadow: 0 1rem 2.5rem rgb(15 23 42 / 8%);
      font-family: Inter, system-ui, sans-serif;
    }

    .label {
      color: #7c3aed;
      font-size: 0.78rem;
      font-weight: 800;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    h2 {
      margin: 0.5rem 0;
      color: #172033;
      font-size: 2rem;
    }

    .summary {
      color: #475569;
      line-height: 1.6;
    }
  `,

What do these styles change?

  • The section selector creates the white dashboard panel.
  • The label selector gives the remote identity a compact purple treatment.
  • The heading plus summary styles establish the page's content hierarchy.
  • Save dashboard-page.ts.
  • Return to the dashboard browser tab.

You should see the dashboard inside a white rounded panel. The Dashboard remote label should appear in purple above the heading.

Panel styles not applied?

Check that the styles property sits inside the @Component metadata. Confirm that a comma separates it from the template property.

Help me repair the DashboardPage component metadata.

The outer panel now has a clear boundary. The remaining selectors turn each metric into a responsive card.

  • Find the closing backtick of the styles property.
  • Insert these selectors immediately before that closing backtick:
    .metrics {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
      gap: 1rem;
      margin-top: 1.5rem;
    }

    article {
      display: grid;
      gap: 0.5rem;
      padding: 1.25rem;
      border: 1px solid #e2e8f0;
      border-radius: 0.8rem;
      background: #f8fafc;
    }

    span {
      color: #64748b;
    }

    strong {
      color: #172033;
      font-size: 1.75rem;
    }

How do the metric styles work?

  • The metrics grid adapts its columns to the available width.
  • Each article becomes a bordered metric card.
  • The label plus value styles separate each metric name from its result.
  • Save dashboard-page.ts.
  • Return to the dashboard browser tab.

The dashboard should now show three responsive cards. Each card should have a muted label plus a larger value.

Metric cards still look plain?

Confirm that the new selectors remain inside the existing styles backticks. Check that the template uses class="metrics" for the card container.

Help me debug the dashboard metric card styles.

✔️ Awesome, I've got everything!

Your dashboard-page.ts file now contains the complete remote-owned view. Make sure the file is saved before continuing.

ⓧ I'd like to double check the full code

import { Component } from '@angular/core';

@Component({
  selector: 'app-dashboard-page',
  template: `
    <section>
      <p class="label">Dashboard remote</p>
      <h2>Platform health</h2>
      <p class="summary">
        This view is built and served independently, then loaded into the shell
        at runtime.
      </p>

      <div class="metrics">
        <article>
          <span>API availability</span>
          <strong>99.98%</strong>
        </article>
        <article>
          <span>Open incidents</span>
          <strong>2</strong>
        </article>
        <article>
          <span>Deployments today</span>
          <strong>14</strong>
        </article>
      </div>
    </section>
  `,
  styles: `
    section {
      padding: 2rem;
      border-radius: 1rem;
      background: #ffffff;
      box-shadow: 0 1rem 2.5rem rgb(15 23 42 / 8%);
      font-family: Inter, system-ui, sans-serif;
    }

    .label {
      color: #7c3aed;
      font-size: 0.78rem;
      font-weight: 800;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    h2 {
      margin: 0.5rem 0;
      color: #172033;
      font-size: 2rem;
    }

    .summary {
      color: #475569;
      line-height: 1.6;
    }

    .metrics {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
      gap: 1rem;
      margin-top: 1.5rem;
    }

    article {
      display: grid;
      gap: 0.5rem;
      padding: 1.25rem;
      border: 1px solid #e2e8f0;
      border-radius: 0.8rem;
      background: #f8fafc;
    }

    span {
      color: #64748b;
    }

    strong {
      color: #172033;
      font-size: 1.75rem;
    }
  `,
})
export class DashboardPage {}

What should match?

Compare the template plus both style groups with your saved file. The metric values should remain 99.98%, 2, and 14.

  • Return to the PowerShell terminal running the dashboard.
  • Stop the development server by pressing Ctrl+C.
Install and initialize Native Federation

The dashboard now owns a complete feature view. Native Federation adds the build configuration plus runtime metadata that lets another Angular application discover it.

  • Install Native Federation 22.2.2 as an exact development dependency by running:
npm install --save-dev --save-exact @angular-architects/native-federation@22.2.2

What does this command do?

The command adds Native Federation to the workspace's development dependencies. The exact version pin keeps its generated configuration aligned with Angular CLI 22.2.2.

  • Select package.json in the Visual Studio Code Explorer sidebar.
  • Confirm that the development dependencies include @angular-architects/native-federation at 22.2.2.

The dependency is now pinned for the whole workspace. The shell can use the same adapter version when it becomes a dynamic host.

Installation failed?

Check that the PowerShell prompt is inside the angular-mfe-lab workspace. Confirm that the dashboard server released the terminal before retrying.

Help me troubleshoot the Native Federation installation.

The initialization schematic converts the existing dashboard project into a remote. It also separates federation startup from Angular application bootstrap.

  • Initialize the dashboard as a remote on port 4201 by running:
ng g @angular-architects/native-federation:init --project dashboard --port 4201 --type remote

What does the initializer generate?

  • The dashboard receives federation.config.mjs for its remote contract.
  • The generated main.ts initializes federation before loading Angular.
  • The generated bootstrap.ts contains the Angular application bootstrap.
  • The generated tsconfig.federation.json supplies federation-specific TypeScript settings.
  • Expand projects/dashboard in the Explorer sidebar.
  • Confirm that federation.config.mjs appears in the dashboard folder.
  • Confirm that bootstrap.ts appears in projects/dashboard/src.
  • Confirm that tsconfig.federation.json appears in the dashboard folder.

That is the federation boundary in place. The dashboard now initializes its remote runtime before bootstrapping the standalone Angular application.

Generated files missing?

Confirm that the initializer command names the existing dashboard project. Refresh the Explorer sidebar if PowerShell reports successful updates but the files are not visible.

Help me verify the Native Federation schematic output.

Publish the dashboard component contract

A remote contract connects exact identifiers. The remote name identifies the application while the exposed key identifies the module that consumers request.

The generated sharing policy also keeps framework packages as shared singleton dependencies. This keeps one compatible Angular runtime active when the shell and remote share a browser page.

  • Select projects/dashboard/federation.config.mjs in the Explorer sidebar.
  • Replace the generated configuration with this remote contract:
import {
  withNativeFederation,
  fromPackageJson,
} from '@angular-architects/native-federation/config';

export default withNativeFederation({
  name: 'dashboard',
  exposes: {
    './Component': './projects/dashboard/src/app/dashboard-page.ts',
  },
  shared: fromPackageJson({
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    build: 'package',
  }).patch(['@angular/core'], {
    includeSecondaries: { keepAll: true },
  }),
  skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],
  features: {
    denseChunking: true,
  },
});

How does this contract work?

  • The remote name dashboard becomes the identity requested by a consuming runtime.
  • The exposed key ./Component points to dashboard-page.ts.
  • The sharing configuration enforces singleton packages with strict compatible versions.
  • The Angular core patch includes its secondary entry points in the shared contract.
  • Save federation.config.mjs.

✔️ Awesome, I've got everything!

Your remote contract names dashboard and exposes ./Component. Make sure the configuration is saved before starting the remote.

ⓧ I'd like to double check the full code

import {
  withNativeFederation,
  fromPackageJson,
} from '@angular-architects/native-federation/config';

export default withNativeFederation({
  name: 'dashboard',
  exposes: {
    './Component': './projects/dashboard/src/app/dashboard-page.ts',
  },
  shared: fromPackageJson({
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    build: 'package',
  }).patch(['@angular/core'], {
    includeSecondaries: { keepAll: true },
  }),
  skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],
  features: {
    denseChunking: true,
  },
});

What should match?

Compare the remote name plus exposure path with your saved file. Keep the generated strict singleton sharing configuration unchanged.

Before you run this, predict whether one development server can provide both the standalone dashboard and its federation metadata.

  • Start the federated dashboard by running this command:
ng serve dashboard

What happens when the remote starts?

The Angular development server serves the standalone dashboard application. Native Federation publishes the remote metadata from the same origin.

  • Open the local dashboard URL printed in PowerShell.
  • Confirm that the Platform health panel displays three metric cards.
  • Open http://localhost:4201/remoteEntry.json in a separate browser tab.

You should see federation metadata at remoteEntry.json. This proves that the dashboard now publishes a runtime contract alongside its standalone page.

Strong finish. The dashboard is independently served on port 4201 while exposing ./Component for runtime loading.

Remote metadata not loading?

Keep the dashboard development server running while you open remoteEntry.json. Confirm that the PowerShell output reports port 4201.

Check that the exposure path ends with projects/dashboard/src/app/dashboard-page.ts. The remote can only publish a component from a valid file path.

Help me debug the dashboard remote metadata.

Your dashboard now has an independent feature view plus a published federation contract. Next, the shell will discover that contract at runtime and expose what happens when the remote disappears.

Connect the Shell at Runtime

Your dashboard already runs as an independent Angular application. Your shell already owns the stable navigation that surrounds each feature.

This step connects them through a deployment-time runtime manifest using Native Federation. You will then test the boundary created by loading an independently served feature through a lazy route.

In this step, get ready to:
  • Initialize the shell as a manifest-driven dynamic host.
  • Pass the resolved federation runtime into the shell route factory.
  • Exercise the boundary between the shell server and the dashboard server.
Initialize the shell as a dynamic host

A dynamic host discovers remote locations when it starts. This keeps the remote URL outside the shell build.

  • Use the plus button in Visual Studio Code's terminal panel to create another Windows PowerShell terminal.
  • Initialize the shell as a dynamic host by running this command:
ng g @angular-architects/native-federation:init --project shell --port 4200 --type dynamic-host

What does this command generate?

  • The schematic configures the shell to use Native Federation on port 4200.
  • It creates the shell bootstrap split across main.ts and bootstrap.ts.
  • It creates tsconfig.federation.json for the shell's federation build.
  • It creates federation.manifest.json for deployment-time remote discovery.
  • Confirm the shell folder now contains src/bootstrap.ts and tsconfig.federation.json in the Visual Studio Code Explorer.
  • Confirm projects/shell/public now contains federation.manifest.json.
  • Start the generated shell configuration by running this command:
ng serve shell

What does this command prove?

The shell dev server compiles through the generated federation build target. A successful Home page confirms the dynamic-host initialization preserved the shell's local route.

  • Open the shell URL printed in the PowerShell terminal.

You should see the Operations Portal header above the shell-owned Home panel. That confirms the generated host still serves the local experience.

  • Press Ctrl+C in the shell PowerShell terminal to stop the generated server.

Did the dynamic-host setup fail?

Confirm the PowerShell terminal is inside the open angular-mfe-lab workspace. Confirm the Native Federation dependency remains installed as an exact development dependency.

Share the terminal output with help me diagnose the shell dynamic-host initialization.

Connect the manifest and lazy route

The resolved NativeFederationResult carries the runtime loader created during initialization. Passing that object through bootstrap gives the route factory typed access to the remote contract.

  • Select projects/shell/src/main.ts in the Visual Studio Code Explorer.
  • Replace the generated file with this runtime initialization:
import {
  initFederation,
  NativeFederationResult,
} from '@angular-architects/native-federation';

initFederation('federation.manifest.json', {
  hostRemoteEntry: { url: './remoteEntry.json' },
})
  .then((nf: NativeFederationResult) =>
    import('./bootstrap').then((module) => module.bootstrap(nf)),
  )
  .catch((error) => console.error(error));

How does federation start?

  • The call to initFederation() reads the manifest before Angular starts.
  • The resolved nf object holds the initialized federation runtime.
  • The dynamic import passes that runtime into the shell's bootstrap() function.
  • Select projects/shell/src/bootstrap.ts in the Visual Studio Code Explorer.
  • Replace the generated bootstrap module with this runtime handoff:
import { NativeFederationResult } from '@angular-architects/native-federation';
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';

export const bootstrap = (nf: NativeFederationResult) =>
  bootstrapApplication(App, appConfig(nf)).catch((error) =>
    console.error(error),
  );

How does bootstrap pass the runtime?

  • The bootstrap() function receives the initialized runtime as nf.
  • The call to appConfig(nf) moves that runtime into Angular's application providers.
  • The final catch records any shell bootstrap failure in the browser console.
  • Select projects/shell/src/app/app.config.ts in the Visual Studio Code Explorer.
  • Replace the existing configuration with this runtime-aware factory:
import {
  ApplicationConfig,
  provideBrowserGlobalErrorListeners,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { NativeFederationResult } from '@angular-architects/native-federation';
import { routes } from './app.routes';

export const appConfig = (
  nf: NativeFederationResult,
): ApplicationConfig => ({
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes(nf)),
  ],
});

How does the router receive the runtime?

  • The application configuration becomes a function that accepts nf.
  • The call to routes(nf) gives the route factory access to the typed remote loader.
  • The global error provider keeps browser errors connected to Angular's error handling.
  • Select projects/shell/public/federation.manifest.json in the Visual Studio Code Explorer.
  • Replace the generated manifest with this dashboard mapping:
{
  "dashboard": "http://localhost:4201/remoteEntry.json"
}

How does the manifest connect the applications?

  • The dashboard key matches the remote name in the dashboard federation configuration.
  • The URL points the shell to the dashboard's federation metadata on port 4201.
  • Changing this deployment-time mapping can redirect discovery without rebuilding the shell.
  • Select projects/shell/src/app/app.routes.ts in the Visual Studio Code Explorer.
  • Replace the file with this import group and remote contract:
import { Type } from '@angular/core';
import { Routes } from '@angular/router';
import { NativeFederationResult } from '@angular-architects/native-federation';
import { Home } from './home';

type DashboardRemote = {
  DashboardPage: Type<unknown>;
};

What does the remote contract describe?

  • The DashboardRemote type describes the module shape expected from the remote.
  • The DashboardPage property must contain an Angular component constructor.
  • This type gives the runtime loader a checked result before the component reaches the router.
  • Place this route factory directly below the DashboardRemote type:
export const routes = (nf: NativeFederationResult): Routes => [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'home',
  },
  {
    path: 'home',
    component: Home,
  },
  {
    path: 'dashboard',
    loadComponent: () =>
      nf
        .as<DashboardRemote>()
        .loadRemoteModule('dashboard', './Component')
        .then((module) => module.DashboardPage),
  },
  {
    path: '**',
    redirectTo: 'home',
  },
];

How does lazy loading cross the boundary?

  • The Home route remains local to the shell.
  • The Dashboard route waits until navigation before calling loadRemoteModule().
  • The remote name dashboard matches the manifest key.
  • The exposed key ./Component matches the dashboard federation configuration.
  • Save all five changed shell files.

Before you start the shell, do you expect the Home route to remain local while federation initializes in the background?

  • Start the connected shell by running this command in its PowerShell terminal:
ng serve shell

What does this run verify?

A successful compile proves that main.ts, bootstrap.ts, app.config.ts, and app.routes.ts agree on the runtime data flow. Loading Home also initializes federation before the first remote navigation.

  • Open the shell URL printed in the PowerShell terminal.

You should see the Operations Portal header with the active Home link. The local welcome panel should remain visible below it.

  • Keep the browser on the Home route for the boundary check.

Did the connected shell fail to compile?

Check that each file uses the parameter name nf. Check that appConfig(nf) and routes(nf) preserve the same handoff.

Use help me trace the NativeFederationResult through my shell if the compiler reports a mismatched signature.

Use this comparison to confirm the connected shell files before testing the remote boundary.

✔️ Awesome, I've got everything!

Great. Keep the shell on Home so the dashboard module remains unloaded for the next check.

ⓧ I'd like to double check the full code

import {
  initFederation,
  NativeFederationResult,
} from '@angular-architects/native-federation';

initFederation('federation.manifest.json', {
  hostRemoteEntry: { url: './remoteEntry.json' },
})
  .then((nf: NativeFederationResult) =>
    import('./bootstrap').then((module) => module.bootstrap(nf)),
  )
  .catch((error) => console.error(error));

What should match in main.ts?

The manifest initializes before bootstrap. The resolved runtime is passed into module.bootstrap(nf).

import { NativeFederationResult } from '@angular-architects/native-federation';
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';

export const bootstrap = (nf: NativeFederationResult) =>
  bootstrapApplication(App, appConfig(nf)).catch((error) =>
    console.error(error),
  );

What should match in bootstrap.ts?

The exported function accepts nf. It passes that value into appConfig(nf).

import {
  ApplicationConfig,
  provideBrowserGlobalErrorListeners,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { NativeFederationResult } from '@angular-architects/native-federation';
import { routes } from './app.routes';

export const appConfig = (
  nf: NativeFederationResult,
): ApplicationConfig => ({
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes(nf)),
  ],
});

What should match in app.config.ts?

The configuration is a function. Its router provider receives the result of routes(nf).

{
  "dashboard": "http://localhost:4201/remoteEntry.json"
}

What should match in the manifest?

The manifest has one remote named dashboard. Its URL points to the federation metadata served on port 4201.

import { Type } from '@angular/core';
import { Routes } from '@angular/router';
import { NativeFederationResult } from '@angular-architects/native-federation';
import { Home } from './home';

type DashboardRemote = {
  DashboardPage: Type<unknown>;
};

export const routes = (nf: NativeFederationResult): Routes => [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'home',
  },
  {
    path: 'home',
    component: Home,
  },
  {
    path: 'dashboard',
    loadComponent: () =>
      nf
        .as<DashboardRemote>()
        .loadRemoteModule('dashboard', './Component')
        .then((module) => module.DashboardPage),
  },
  {
    path: '**',
    redirectTo: 'home',
  },
];

What should match in app.routes.ts?

The route factory receives nf. The Dashboard route resolves module.DashboardPage through the typed runtime loader.

Test the independent failure boundary

The shell and dashboard now form one browser experience while retaining separate development servers. This check reveals which parts truly belong to the shell.

  • Return to the dashboard PowerShell terminal from earlier.
  • Confirm the dashboard server is still serving on port 4201.
  • Press Ctrl+C in the dashboard PowerShell terminal to stop only that server.
  • Leave the shell PowerShell terminal running on port 4200.

Before you select Dashboard, do you think stopping the feature server will remove the whole portal or only affect the routed feature?

  • Select Dashboard in the shell navigation.

The dashboard cards do not render because the lazy remote cannot load. You should still see the Operations Portal header with the Home and Dashboard navigation links.

You have exposed the real deployment boundary. The remote disappeared while the shell-owned frame stayed usable.

Your runtime composition now works across an independent server boundary. Next, you will turn that rejected lazy load into a shell-owned recovery view.

Add a Shell-Owned Fallback

Your shell header survived the failed remote navigation. That result proves the stable Angular frame remains available when the dashboard disappears.

This step turns the rejected lazy load into a shell-owned failure boundary. The typed Native Federation contract stays intact while the routed area gains a recovery view.

In this step, get ready to:
  • Build a shell-owned recovery component.
  • Route rejected dashboard loads to the recovery component.
  • Validate recovery across runtime failure and production compilation.
Create the recovery component

The recovery view must live inside the shell because the dashboard cannot supply UI while it is unavailable. Its message gives the learner a clear next action without depending on remote code.

  • Use the file creation control in Visual Studio Code to create projects/shell/src/app/remote-unavailable.ts.
  • Add the shell-owned recovery markup by entering this code:
import { Component } from '@angular/core';

@Component({
  selector: 'app-remote-unavailable',
  template: `
    <section class="fallback" role="alert">
      <p class="status">Remote load failed</p>
      <h2>Dashboard temporarily unavailable</h2>
      <p>
        The shell is still running. Start the dashboard server and refresh this
        route to try again.
      </p>
    </section>
  `,
})
export class RemoteUnavailable {}

What does this component do?

  • The RemoteUnavailable component belongs to the shell deployment.
  • The alert role identifies the panel as an important status update.
  • The message confirms that the shell is still running.
  • The retry guidance tells the learner how to restore the remote view.
  • Save remote-unavailable.ts.
  • Return to the shell PowerShell terminal to check the compilation result.

You should see the shell finish compiling without a TypeScript error from the new component.

Seeing a component error?

  • Confirm that remote-unavailable.ts sits inside projects/shell/src/app.
  • Compare the closing backtick after the template with the opening backtick after template:.
  • Check that the exported class is named RemoteUnavailable.

Ask for help with the component syntax:

A recovery state also needs a visual signal that separates it from normal dashboard content. The amber panel keeps the status noticeable while preserving the shell layout.

  • Insert this styles property after the template inside the component metadata:
  styles: `
    .fallback {
      padding: 2rem;
      border: 1px solid #fbbf24;
      border-radius: 1rem;
      background: #fffbeb;
      color: #78350f;
    }

    .status {
      font-weight: 800;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    h2 {
      margin: 0.5rem 0;
    }
  `,

How does the recovery panel communicate status?

  • The border and background distinguish the fallback from the normal dashboard cards.
  • The darker text keeps the message readable against the amber background.
  • The uppercase status line makes the remote failure easy to identify.
  • Save remote-unavailable.ts.
  • Return to the shell PowerShell terminal to confirm the updated component compiles.

You should see another successful compilation after the styles are added. Your shell now owns a complete recovery view.

Styles causing a compilation error?

  • Confirm that the styles property sits before the closing }).
  • Check that the styles start with a backtick after styles:.
  • Check that the styles end with a backtick followed by a comma.

Help me compare the component structure with the expected file.

✔️ Awesome, I've got everything!

Great. Double-check that you saved remote-unavailable.ts.

ⓧ I'd like to double check the full code

import { Component } from '@angular/core';

@Component({
  selector: 'app-remote-unavailable',
  template: `
    <section class="fallback" role="alert">
      <p class="status">Remote load failed</p>
      <h2>Dashboard temporarily unavailable</h2>
      <p>
        The shell is still running. Start the dashboard server and refresh this
        route to try again.
      </p>
    </section>
  `,
  styles: `
    .fallback {
      padding: 2rem;
      border: 1px solid #fbbf24;
      border-radius: 1rem;
      background: #fffbeb;
      color: #78350f;
    }

    .status {
      font-weight: 800;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    h2 {
      margin: 0.5rem 0;
    }
  `,
})
export class RemoteUnavailable {}

What should you compare?

Check the template delimiters first. Then compare the placement of the styles property and the RemoteUnavailable class name.

Catch the remote load rejection

The dashboard route already returns the remote component when loading succeeds. A rejection handler can return RemoteUnavailable through the same component-loading contract.

The route needs access to the new shell component before it can use that recovery path. You will add the import before changing the lazy-load promise.

  • Return to projects/shell/src/app/app.routes.ts.
  • Find this existing import:
import { Home } from './home';

What are you locating?

This import gives the route configuration access to the shell-owned Home component. The fallback import belongs beside it because both views live in the shell.

  • Add the fallback import directly below the Home import so the two lines look like this:
import { Home } from './home';
import { RemoteUnavailable } from './remote-unavailable';

What does the new import enable?

The route factory can now resolve a failed dashboard load to RemoteUnavailable. The recovery component remains part of the shell bundle.

  • Scroll to the dashboard route.
  • Find the current promise ending:
        .loadRemoteModule('dashboard', './Component')
        .then((module) => module.DashboardPage),

How does the current route finish?

The typed loader resolves the dashboard exposure. The final promise step extracts DashboardPage for Angular to render.

  • Replace that promise ending with this version:
        .loadRemoteModule('dashboard', './Component')
        .then((module) => module.DashboardPage)
        .catch(() => RemoteUnavailable),

How does the failure boundary work?

  • A successful load still resolves to DashboardPage.
  • A rejected load now resolves to RemoteUnavailable.
  • Both outcomes give Angular a component type for the routed area.
  • The manifest key and exposed module contract remain unchanged.
  • Save app.routes.ts.
  • Return to the shell PowerShell terminal to check the route compilation.

You should see the shell compile without an import error or route type error.

Route no longer compiling?

  • Confirm that the import path ends with ./remote-unavailable.
  • Check that RemoteUnavailable uses the same spelling in the import and rejection handler.
  • Check that the comma appears after the completed promise chain.

Help me diagnose the fallback route error.

✔️ Awesome, I've got everything!

Your dashboard route now has success and recovery outcomes. Save app.routes.ts before testing it.

ⓧ I'd like to double check the full code

import { Type } from '@angular/core';
import { Routes } from '@angular/router';
import { NativeFederationResult } from '@angular-architects/native-federation';
import { Home } from './home';
import { RemoteUnavailable } from './remote-unavailable';

type DashboardRemote = {
  DashboardPage: Type<unknown>;
};

export const routes = (nf: NativeFederationResult): Routes => [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'home',
  },
  {
    path: 'home',
    component: Home,
  },
  {
    path: 'dashboard',
    loadComponent: () =>
      nf
        .as<DashboardRemote>()
        .loadRemoteModule('dashboard', './Component')
        .then((module) => module.DashboardPage)
        .catch(() => RemoteUnavailable),
  },
  {
    path: '**',
    redirectTo: 'home',
  },
];

What should you compare?

Confirm that the fallback import appears below the Home import. Then compare the complete dashboard promise chain with your route.

Validate failure, recovery, and builds

A clean failure test needs fresh process state because a remote module can remain available after its first successful load. The shell must initialize federation on Home before the dashboard server stops.

  • Return to the Dashboard PowerShell terminal from the previous step.
  • Stop the dashboard process using the same terminal interruption you used in the previous failure test.
  • Return to the shell PowerShell terminal from the previous step.
  • Stop the shell process using the same terminal interruption.
  • Restart Dashboard on port 4201 by running this command:
ng serve dashboard

What does this command start?

This starts the independently served dashboard application. Its development server also serves the federation metadata used by the shell.

Dashboard server not starting?

  • Confirm that the PowerShell terminal is still inside angular-mfe-lab.
  • Confirm that the earlier dashboard process has stopped before retrying the command.

Help me diagnose why the dashboard development server is not starting.

  • Restart Shell on port 4200 from its PowerShell terminal by running this command:
ng serve shell

What does this command start?

This starts the dynamic host. The shell reads federation.manifest.json before it creates the routed application.

Shell server not starting?

  • Confirm that the shell terminal is inside angular-mfe-lab.
  • Check the terminal output for a file path that points to the route or fallback component.
  • Confirm that the previous shell process released port 4200.

Help me diagnose why the shell development server is not starting.

  • Open the local shell URL printed in its PowerShell terminal.
  • Select the Home link in the shell navigation.
  • Wait for the shell-owned welcome panel to render.
  • Return to the Dashboard PowerShell terminal.
  • Stop only the dashboard process using the same terminal interruption.

Before you select Dashboard, consider which view the shell can render after the remote has stopped.

  • Select the Dashboard link in the shell navigation.

You should see Dashboard temporarily unavailable inside the routed area. The Operations Portal header and both navigation links should remain visible.

The fallback proves that the shell can absorb an independently failing deployment. The next check confirms that recovery still works when the remote returns.

  • Return to the Dashboard PowerShell terminal.
  • Restart Dashboard by running this command:
ng serve dashboard

Why restart the remote?

Restarting Dashboard restores the manifest target on port 4201. A fresh shell page can initialize the remote metadata again.

Dashboard still unavailable after restarting?

  • Wait for the dashboard terminal to finish compiling before refreshing the shell.
  • Confirm that the dashboard terminal prints a local URL using port 4201.
  • Confirm that the shell manifest still maps dashboard to http://localhost:4201/remoteEntry.json.

Help me diagnose why the dashboard still shows the fallback after its server restarted.

  • Refresh the shell page in your browser.

You should see the remote-owned Platform health view with API availability, open incidents, and deployments today. That closes the recovery loop: the shell degrades safely and loads the real remote again when it returns.

The runtime checks prove both component outcomes. Production builds now verify that the dashboard exposure and shell fallback compile as independent targets.

  • Return to the Dashboard PowerShell terminal.
  • Stop the dashboard development server.
  • Return to the shell PowerShell terminal.
  • Stop the shell development server.
  • Return to a PowerShell terminal already inside angular-mfe-lab.

Before the first build, consider whether the independently configured dashboard can compile without the shell running.

  • Build the dashboard target by running this command:
ng build dashboard

What does this build verify?

This compiles the dashboard as its own application target. It also validates the exposed dashboard component and remote federation configuration.

You should see the dashboard build finish successfully without a compilation error.

Dashboard build failing?

  • Check the first reported file path in the build output.
  • Compare projects/dashboard/federation.config.mjs with the working remote configuration from the previous step.
  • Confirm that projects/dashboard/src/app/dashboard-page.ts still exports DashboardPage.

Help me diagnose the dashboard production build failure.

Before the final build, consider whether the shell's fallback import and typed route can compile together for production.

  • Build the shell production target by running this command:
ng build shell --configuration production

What does the shell build verify?

  • The build compiles the dynamic host with its runtime manifest initialization.
  • The build type-checks the typed dashboard contract.
  • The build includes the shell-owned RemoteUnavailable component.
  • The build validates both branches of the lazy component promise.

You should see the shell production build finish successfully. Strong finish. Your shell now survives a missing remote and returns to the real dashboard after recovery.

Shell production build failing?

  • Check whether the first reported path points to app.routes.ts or remote-unavailable.ts.
  • Confirm that the route imports RemoteUnavailable from ./remote-unavailable.
  • Confirm that the rejection handler returns the component class without constructing it.

Help me diagnose the shell production build failure.

Secret mission

Register a Reports Remote

Add a second independently served Reports remote on port 4202. Register it in the runtime manifest so its Release summary renders inside the existing Operations Portal shell.

Clean Up Your Resources

Clean Up Your Resources

Everything in this project runs locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them for later, or delete them entirely.

Resources you used:

  • The local angular-mfe-lab Angular workspace.
  • The Shell, Dashboard, and Reports development servers in Windows PowerShell.

Keep everything running

No action needed. Choose this if you are still testing runtime composition or extending the remotes.

  • Keep the development servers running in their existing Windows PowerShell terminals.
  • Keep the angular-mfe-lab workspace open in Visual Studio Code.

Pause - I'll come back to this later

Shut down the local development servers to free their ports. Your workspace stays ready for your next session.

  • Return to the Windows PowerShell terminal running Shell.
  • Press Ctrl+C to stop the Shell server.
  • Return to the Windows PowerShell terminal running Dashboard.
  • Press Ctrl+C to stop the Dashboard server.
  • Return to the Windows PowerShell terminal running Reports.
  • Press Ctrl+C to stop the Reports server.
  • Leave the angular-mfe-lab folder on your computer so you can restart the servers later.

Delete - I don't want to use this again

This option permanently removes the local workspace. Your computer keeps no cloud resources or ongoing services from this project.

  • Stop each development server by pressing Ctrl+C in its Windows PowerShell terminal.
  • Close Visual Studio Code.
  • Press the Windows key to open Windows search.
  • Type File Explorer and press Enter to open it.
  • Return to the location that contains the angular-mfe-lab folder.
  • Right-click the angular-mfe-lab folder.
  • Select Delete.
  • Confirm that angular-mfe-lab no longer appears in File Explorer.

Nice Work!

Nice Work!

You did it! You built a resilient Angular microfrontend system that keeps its shell usable when an independently deployed feature cannot load.

Your Operations Portal now discovers independently served features at runtime. The shell retains control of its navigation plus recovery view.

You learned how to:

  • Build a persistent shell with local Home routing. Keep the Operations Portal navigation outside the routed feature area.
  • Load an independently served Dashboard through Native Federation. Use a runtime manifest for deployment-time discovery. Compose browser modules through ES modules plus import maps. Protect Angular dependencies with shared singletons.
  • Prove a failure boundary by stopping Dashboard before its first lazy navigation. Preserve shell navigation with a shell-owned fallback. Produce successful production builds for Dashboard plus Shell.
  • Secret Mission: Registered a second independently served Reports remote. Extended the same manifest contract without changing the shell federation builder configuration.

Ready to quiz yourself?