Skip to content

Getting Started

Taras Demyanets edited this page Mar 31, 2026 · 1 revision

Getting Started

This guide walks you through installing and configuring @microfrontend in both a shell application and a microfrontend application.

Prerequisites

  • Node.js (LTS recommended)
  • A SPA framework of your choice (Angular, React, Vue, etc.)
  • Hash-based routing enabled in your microfrontend's router

Installation

Shell application (host)

npm install @microfrontend/common @microfrontend/controller

Microfrontend application (child)

npm install @microfrontend/common @microfrontend/client

Step 1: Configure the Shell

The shell application creates a MetaRouter that manages all microfrontends.

1.1 Define routes

Each microfrontend is identified by a metaRoute name and a baseUrl pointing to where the microfrontend is served:

import { IAppConfig } from '@microfrontend/controller';

const routes: IAppConfig[] = [
  {
    metaRoute: 'app-a',
    baseUrl: 'https://app-a.example.com'
  },
  {
    metaRoute: 'app-b',
    baseUrl: 'https://app-b.example.com'
  }
];

1.2 Create the MetaRouter

import { MetaRouter, MetaRouterConfig, FrameConfig } from '@microfrontend/controller';
import { Level } from '@microfrontend/common';

const config = new MetaRouterConfig(
  'outlet',                              // outlet name (matches the DOM element id)
  routes,                                 // route definitions
  (metadata, data) => {                   // broadcast notification handler
    console.log('Broadcast received:', metadata.tag, data);
  },
  new FrameConfig(),                      // optional: iframe configuration
  UnknownRouteHandlingEnum.ThrowError,    // optional: unknown route handling
  Level.INFO                              // optional: log level
);

const router = new MetaRouter(config);

1.3 Add the outlet to your HTML

The outlet is a plain DOM element where iframes will be injected:

<div id="outlet" style="height: 100%"></div>

Important: The id attribute must match the outlet parameter in MetaRouterConfig.

1.4 Initialize the router

// Preload creates iframes for all (or specific) routes
await router.preload();

// Initialize starts routing — navigates to the first route or restores from the URL hash
await router.initialize();

1.5 Navigate between microfrontends

// Navigate to microfrontend 'app-a'
await router.go('app-a');

// Navigate to microfrontend 'app-a' with subroute 'settings'
await router.go('app-a', 'settings');

Step 2: Configure the Microfrontend

Each microfrontend uses the RoutedApp class to communicate with the shell.

2.1 Create a RoutedApp instance

import { RoutedApp, RoutedAppConfig } from '@microfrontend/client';
import { Level } from '@microfrontend/common';

const config = new RoutedAppConfig(
  'app-a',                               // metaRoute — must match the shell's route config
  'https://shell.example.com',           // parentOrigin — the shell's origin (for postMessage security)
  Level.INFO                              // optional: log level
);

const routedApp = new RoutedApp(config);

2.2 Report route changes to the shell

Whenever the microfrontend navigates internally, it must report the new route to the shell so the URL hash stays in sync:

// Example with Angular Router
router.events
  .pipe(filter(e => e instanceof NavigationEnd))
  .subscribe((e: NavigationEnd) => {
    routedApp.sendRoute(e.url);
  });

2.3 Handle route changes from the shell

When the user clicks a navigation link in the shell (or uses back/forward buttons), the shell tells the microfrontend which subroute to display:

routedApp.registerRouteChangeCallback((activated, subRoute) => {
  if (subRoute) {
    router.navigateByUrl(subRoute, { replaceUrl: true });
  } else {
    router.navigateByUrl('/', { replaceUrl: true });
  }
});

Key point: Use replaceUrl: true to avoid duplicating history entries — the shell already manages browser history.

2.4 Check if running inside a shell

if (routedApp.hasShell) {
  // Running inside the shell — full microfrontend features available
} else {
  // Running standalone — graceful degradation
}

Step 3: Enable Hash-Based Routing

Microfrontends must use hash-based routing so that the shell can manage the main URL path while each microfrontend controls its own hash fragment.

Angular

RouterModule.forRoot(routes, { useHash: true })

React (react-router)

import { HashRouter } from 'react-router-dom';

<HashRouter>
  <App />
</HashRouter>

Vue

const router = createRouter({
  history: createWebHashHistory(),
  routes
});

Complete Shell Example (Angular)

import { Component, OnInit } from '@angular/core';
import { Level } from '@microfrontend/common';
import {
  FrameConfig,
  IAppConfig,
  MetaRouter,
  MetaRouterConfig,
  UnknownRouteHandlingEnum
} from '@microfrontend/controller';

const routes: IAppConfig[] = [
  { metaRoute: 'a', baseUrl: 'http://localhost:30307' },
  { metaRoute: 'b', baseUrl: 'http://localhost:30809' }
];

@Component({
  selector: 'app-root',
  template: `
    <nav>
      <a (click)="go('a')">App A</a> |
      <a (click)="go('b')">App B</a> |
      <a (click)="go('a', 'settings')">App A — Settings</a>
    </nav>
    <div id="outlet" style="height: 100%"></div>
  `
})
export class AppComponent implements OnInit {
  router: MetaRouter;

  constructor() {
    const config = new MetaRouterConfig(
      'outlet',
      routes,
      (metadata, data) => console.log('Broadcast:', metadata.tag, data),
      new FrameConfig({}, {}, { class: 'my-outlet-frame' }),
      UnknownRouteHandlingEnum.ThrowError,
      Level.LOG
    );
    this.router = new MetaRouter(config);
  }

  async ngOnInit(): Promise<void> {
    await this.router.preload();
    await this.router.initialize();
  }

  async go(route: string, subRoute?: string): Promise<void> {
    await this.router.go(route, subRoute);
  }
}

Complete Microfrontend Example (Angular)

// app.tokens.ts
import { InjectionToken } from '@angular/core';
import { RoutedApp } from '@microfrontend/client';

export const ROUTED_APP = new InjectionToken<RoutedApp>('ROUTED_APP');

// app.module.ts
import { RoutedApp, RoutedAppConfig } from '@microfrontend/client';
import { Level } from '@microfrontend/common';

const config = new RoutedAppConfig('a', 'http://localhost:30103', Level.LOG);

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot(
      [
        { path: 'settings', component: SettingsComponent },
        { path: 'dashboard', component: DashboardComponent },
        { path: '**', redirectTo: 'dashboard' }
      ],
      { useHash: true }
    )
  ],
  providers: [
    { provide: ROUTED_APP, useFactory: () => new RoutedApp(config) }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

// app.component.ts
@Component({ selector: 'app-root', templateUrl: './app.component.html' })
export class AppComponent {
  constructor(
    @Inject(ROUTED_APP) private routedApp: RoutedApp,
    private router: Router
  ) {
    // Report route changes to the shell
    this.router.events
      .pipe(filter(e => e instanceof NavigationEnd))
      .subscribe((e: NavigationEnd) => this.routedApp.sendRoute(e.url));

    // Handle route changes from the shell
    this.routedApp.registerRouteChangeCallback((activated, subRoute) => {
      this.router.navigateByUrl(subRoute ?? '/', { replaceUrl: true });
    });

    // Handle broadcast messages
    this.routedApp.registerBroadcastCallback((metadata, data) => {
      console.log('Received broadcast:', metadata.tag, data);
    });
  }
}

Next Steps

Clone this wiki locally