Micro Frontends using Module Federation and Angular

Apedu.coApedu.co
20 min read

What is Micro Frontend?

A micro frontend is an architectural design pattern to build robust, highly scalable, distributed applications. Where a monolithic application is broken down into smaller pieces or subdomains and developed and deployed independently.

Do we need it?

The answer is it depends, like when we have a product suite or dashboard so in this kind of application there are lots of things that are domains of their own in such scenarios, we can divide an application into its subdomains and assign a team developing that domain so that can be developed and maintained by the team independently of any other dependencies.

3.jpg

How to implement micro frontend using Angular with module federation

Final Project on GitHub

What we will build

  • Dashboard App ( Host )
  • Auth App ( Remote )
  • Todo App ( Remote )
  • Authentication Library ( Shared Library )
  • Todo Library ( Shared Library )

Create Nx Workspace

we will be using nx workspace to manage our codebase as it provides an intuitive experience to develop micro frontend applications.

npx create-nx-workspace mfe-demo

Add Angular Plugin

To add Angular-related features to our newly created monorepo we need to install the Angular Plugin.

Note: you are inside the mfe-demo directory i.e root directory of the nx workspace

npm i -D @nrwl/angular

Generate Application

We need to generate two applications. We also need to tell Nx that we want these applications to support Module Federation.

Dashboard Application (Host)

npx nx g @nrwl/angular:app dashboard --mfe --mfeType=host --port=4200 --routing=true --style=scss --inlineTemplate=true --inlineStyle=true

Auth Application (Remote)

npx nx g @nrwl/angular:app auth --mfe --mfeType=remote --host=dashboard --port=4201 --routing=true --style=scss --inlineTemplate=true --inlineStyle=true

Todos Application (Remote)

npx nx g @nrwl/angular:app todos --mfe --mfeType=remote --host=dashboard --port=4202 --routing=true --style=scss --inlineTemplate=true --inlineStyle=true

Note: We provided remote as the --mfeType. This tells the generator to create a Webpack configuration file that is ready to be consumed by a Host application.

Note: We provided 4201 as the --port. This helps when developing locally as it will tell the serve target to serve on a different port reducing the chance of multiple remote apps trying to run on the same port.

Note: We provided --host=dashboard as an option. This tells the generator that this remote app will be consumed by the Dashboard application. The generator will automatically link these two apps together in the webpack.config.js

Note: The RemoteEntryModule generated will be imported in the app.module.ts file, however, it is not used in the AppModule itself. This is to allow TS to find the Module during compilation, allowing it to be included in the built bundle. This is required for the Module Federation Plugin to expose the Module correctly. You can choose to import the RemoteEntryModule in the AppModule if you wish, however, it is not necessary.

Key Difference between Host and Remote config files.

Dashboard webpack.config.js file

plugins: [
    new ModuleFederationPlugin({
      remotes: {
        auth: 'http://localhost:4201/authRemoteEntry.js',
        todos: 'http://localhost:4202/todosRemoteEntry.js',
      },
      shared: {
        '@angular/core': { singleton: true, strictVersion: true },
        '@angular/common': { singleton: true, strictVersion: true },
        '@angular/common/http': { singleton: true, strictVersion: true },
        '@angular/router': { singleton: true, strictVersion: true },
        ...sharedMappings.getDescriptors(),
      },
      library: {
        type: 'module',
      },
    }),
    sharedMappings.getPlugin(),
  ],

Auth webpack.config.js file

plugins: [
    new ModuleFederationPlugin({
      name: 'auth',
      filename: 'authRemoteEntry.js',
      exposes: {
        './RemoteEntryModule': 'apps/auth/src/app/remote-entry/entry.module.ts',
      },
      shared: {
        '@angular/core': { singleton: true, strictVersion: true },
        '@angular/common': { singleton: true, strictVersion: true },
        '@angular/common/http': { singleton: true, strictVersion: true },
        '@angular/router': { singleton: true, strictVersion: true },
        ...sharedMappings.getDescriptors(),
      },
      library: {
        type: 'module',
      },
    }),
    sharedMappings.getPlugin(),
  ],

Todos webpack.config.js file

plugins: [
    new ModuleFederationPlugin({
      name: 'todos',
      filename: 'todosRemoteEntry.js',
      exposes: {
        './RemoteEntryModule':
          'apps/todos/src/app/remote-entry/entry.module.ts',
      },
      shared: {
        '@angular/core': { singleton: true, strictVersion: true },
        '@angular/common': { singleton: true, strictVersion: true },
        '@angular/common/http': { singleton: true, strictVersion: true },
        '@angular/router': { singleton: true, strictVersion: true },
        ...sharedMappings.getDescriptors(),
      },
      library: {
        type: 'module',
      },
    }),
    sharedMappings.getPlugin(),
  ],

Adding Functionality

We'll start by building the Auth app, which will consist of a login form and some very basic and insecure authorization logic.

Authentication Library

Let's create a user authentication library that we will share between the host application and the remote application. This will be used to determine if there is an authenticated user as well as provide logic for authenticating the user.

nx g @nrwl/angular:lib shared/authentication

Also we need an Angular Service that we will use to hold state:

nx g @nrwl/angular:service auth --project=shared-authentication

This will create a file auth.service.ts under the shared/authentication library. Change it's contents to match:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { v4 } from "uuid";

// Interfaces

export interface AuthUser {
  uid: string;
  name: string;
  email: string;
}

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  public authUser = new BehaviorSubject<AuthUser | null>(null);

  public currentUser(): Observable<AuthUser | null> {
    return this.authUser;
  }

  public async signIn(credential: {
    email: string;
    password: string;
  }): Promise<void> {
    if (credential.email === 'admin' && credential.password === 'admin') {
      this.authUser.next({
        uid: v4(),
        email: credential.email,
        name: 'Admin',
      });
    }
  }

  public async signUp(userData: {
    email: string;
    password: string;
    name: string;
  }): Promise<void> {
    this.authUser.next({
      uid: v4(),
      email: userData.email,
      name: userData.name,
    });
  }

  public async signOut(): Promise<void> {
    this.authUser.next(null);
  }
}

Add a new export to the shared/authentication index.ts file: export * from './lib/auth.service';

We are done with the Authentication library, Now we will perform a similar procedure for the todos library.

Todo Library

This library will be responsible for storing todos for currently logged-in user and todo CRUD operations.

nx g @nrwl/angular:lib shared/todo

Also, we need an Angular Service that we will use to hold state:

nx g @nrwl/angular:service todo --project=shared-todo

This will create a file todo.service.ts under the shared/todo library. Change its contents to match:

import { Injectable } from '@angular/core';
import { AuthUser } from '@mfe-demo-prototype/shared/authentication';
import { BehaviorSubject, Observable } from 'rxjs';
import { v4 } from 'uuid';

// Interfaces
export interface Todo {
  id: string;
  body: string;
  done: boolean;
}

@Injectable({
  providedIn: 'root',
})
export class UserTodoService {
  private currentUserId: string | undefined;

  public currentUser!: AuthUser;

  private todos: Todo[] = [];

  public todos$ = new BehaviorSubject<Todo[]>([]);
  // Set current User
  public setCurrentUser(data: { uid: string; name: string; email: string }) {
    this.currentUser = data;
  }

  // set User ID
  public setUserId(userUid: string) {
    this.currentUserId = userUid;
  }

  // get Todos
  public getTodos(): Observable<Todo[]> {
    return this.todos$;
  }

  // Create Todo
  public async createTodo(body: string): Promise<void> {
    const newTodo: Todo = {
      id: v4(),
      body,
      done: false,
    };
    this.todos.push(newTodo);
    this.todos$.next(this.todos);
  }
  // update Todo
  public async updateTodo(todo: Todo): Promise<void> {
    this.todos = this.todos.map((t) => {
      if (t.id === todo.id) {
        const updatedTodo: Todo = {
          ...t,
          done: !todo.done,
        };
        return updatedTodo;
      }
      return t;
    });
    this.todos$.next(this.todos);
  }

  // delete Todo
  public async deleteTodo(todo: Todo): Promise<void> {
    this.todos = this.todos.filter((t) => t.id !== todo.id);
    this.todos$.next(this.todos);
  }

  // Clear All Todos When Logged Out
  public async clearTodos(): Promise<void> {
    this.todos = [];
    this.todos$.next(this.todos);
  }
}

Add a new export to the shared/todo index.ts file: export * from './lib/todo.service';

With that we are done with all the library we need for this project.


Now on Auth App

We will be setting up thelogin route and signup route for the user to log in as well as sign up and also the home route for the application.

Create those components

Login Component

nx g @nrwl/angular:component remote-entry/components/login --project auth --module=entry --inlineTemplate=true --inlineStyle=true

Now open the login.component.ts file and change its content to match:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';

@Component({
  selector: 'mfe-demo-prototype-login',
  template: `
    <div class="flex items-center justify-center h-screen bg-gray-200">
      <div
        class="flex flex-col items-center justify-start bg-gray-300 rounded-xl shadow-2xl p-4 w-64"
      >
        <div class="flex items-center justify-center shadow-md rounded-full">
          <!-- Image -->
          <svg
            xmlns="http://www.w3.org/2000/svg"
            class="h-20 w-20 text-green-500"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
            />
          </svg>
        </div>
        <form
          [formGroup]="loginForm"
          (submit)="submit()"
          class="flex flex-col items-start justify-center space-y-4 w-full"
        >
          <div class="flex flex-col items-start justify-start w-full">
            <label for="email" class="text-xs font-semibold my-1"
              >User Name</label
            >
            <input
              type="email"
              name="email"
              class="rounded focus:outline-none p-1 w-full"
              formControlName="email"
            />
          </div>
          <div class="flex flex-col items-start justify-start w-full">
            <label for="passowrd" class="text-xs font-semibold my-1">
              Password
            </label>
            <input
              type="password"
              name="password"
              class="rounded focus:outline-none p-1 w-full"
              formControlName="password"
            />
          </div>

          <button
            type="submit"
            class="p-1 bg-green-600 rounded-lg w-full font-bold text-gray-50"
          >
            Login
          </button>
        </form>
      </div>
    </div>
  `,
  styles: [],
})
export class LoginComponent implements OnInit {
  public loginForm!: FormGroup;

  constructor(private fb: FormBuilder, private authService: AuthService) {}

  ngOnInit(): void {
    this.loginForm = this.fb.group({
      email: '',
      password: '',
    });

    this.loginForm.valueChanges.subscribe(console.log);
  }

  async submit(): Promise<void> {
    if (this.loginForm.valid) {
      const email = this.loginForm.get('email')?.value;
      const password = this.loginForm.get('password')?.value;
      await this.authService.signIn({ email, password });
    }
  }
}

Signup Component

nx g @nrwl/angular:component remote-entry/components/signup --project auth --module=entry --inlineTemplate=true --inlineStyle=true

Now open the signup.component.ts file and match the content.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';

@Component({
  selector: 'mfe-demo-prototype-signup',
  template: `
    <div class="flex items-center justify-center h-screen bg-gray-200">
      <div
        class="flex flex-col items-center justify-start bg-gray-200 rounded-xl shadow-2xl p-4 w-64"
      >
        <div>
          <!-- Image -->
          <svg
            xmlns="http://www.w3.org/2000/svg"
            class="h-20 w-20 text-green-500"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
            />
          </svg>
        </div>
        <form
          [formGroup]="signUpForm"
          (submit)="submit()"
          class="flex flex-col items-start justify-center space-y-4 w-full"
        >
          <div class="flex flex-col items-start justify-start w-full">
            <label for="name" class="text-xs font-semibold my-1">Name</label>
            <input
              type="text"
              name="name"
              class="rounded focus:outline-none p-1 w-full"
              formControlName="name"
            />
          </div>
          <div class="flex flex-col items-start justify-start w-full">
            <label for="email" class="text-xs font-semibold my-1">
              Email
            </label>
            <input
              type="email"
              name="email"
              class="rounded focus:outline-none p-1 w-full"
              formControlName="email"
            />
          </div>
          <div class="flex flex-col items-start justify-start w-full">
            <label for="passoword" class="text-xs font-semibold my-1">
              Password
            </label>
            <input
              type="password"
              name="password"
              class="rounded focus:outline-none p-1 w-full"
              formControlName="password"
            />
          </div>

          <button
            type="submit"
            class="p-1 bg-green-600 rounded-lg w-full font-bold text-gray-50"
          >
            SignUp
          </button>
        </form>
      </div>
    </div>
  `,
  styles: [],
})
export class SignupComponent implements OnInit {
  public signUpForm!: FormGroup;

  constructor(private fb: FormBuilder, private authService: AuthService) {}

  ngOnInit(): void {
    this.signUpForm = this.fb.group({
      name: '',
      email: '',
      password: '',
    });
  }

  async submit() {
    if (this.signUpForm.valid) {
      const name = this.signUpForm.get('name')?.value;
      const email = this.signUpForm.get('email')?.value;
      const password = this.signUpForm.get('password')?.value;
      await this.authService.signUp({ name, email, password });
    }
  }
}

Home Component

nx g @nrwl/angular:component remote-entry/components/home --project auth --module=entry --inlineTemplate=true --inlineStyle=true

Now open home.component.ts file and match the content:

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

@Component({
  selector: 'mfe-demo-prototype-home',
  template: ` <div class="h-screen flex items-center justify-center bg-gray-200">
    <div class="p-2 shadow-2xl rounded-lg bg-gray-100">
      This Micro App handling only Authentication
    </div>
  </div> `,
  styles: [],
})
export class HomeComponent implements OnInit {
  constructor() {}

  ngOnInit(): void {}
}

App Component

Now open app.component.ts file and match the content:

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

@Component({
  selector: 'mfe-demo-prototype-root',
  template: ` <router-outlet></router-outlet>`,
  styles: [],
})
export class AppComponent {}

App Module

Also, we have to make changes to the app.module.ts file.

/*
 * This RemoteEntryModule is imported here to allow TS to find the Module during
 * compilation, allowing it to be included in the built bundle. This is required
 * for the Module Federation Plugin to expose the Module correctly.
 * */
import { RemoteEntryModule } from './remote-entry/entry.module';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { HashLocationStrategy, PathLocationStrategy } from '@angular/common';

const routes: Routes = [
  {
    path: '',
    loadChildren: () =>
      import('./remote-entry/entry.module').then((m) => m.RemoteEntryModule),
  },
];

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' }),
  ],
  providers: [
    { provide: PathLocationStrategy, useClass: HashLocationStrategy },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

And lastly, edit the webpack.config.js file to expose the module and to ingest the libraries.

Webpack Config

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const path = require('path');

/**
 * We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser
 * builder as it will generate a temporary tsconfig file which contains any required remappings of
 * shared libraries.
 * A remapping will occur when a library is buildable, as webpack needs to know the location of the
 * built files for the buildable library.
 * This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains
 * the location of the generated temporary tsconfig file.
 */
const tsConfigPath =
  process.env.NX_TSCONFIG_PATH ??
  path.join(__dirname, '../../tsconfig.base.json');

const workspaceRootPath = path.join(__dirname, '../../');
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
  tsConfigPath,
  [
    /* mapped paths to share */
    '@mfe-demo-prototype/shared/authentication',
    '@mfe-demo-prototype/shared/todo',
  ],
  workspaceRootPath
);

module.exports = {
  output: {
    uniqueName: 'auth',
    publicPath: 'auto',
  },
  optimization: {
    runtimeChunk: false,
  },
  experiments: {
    outputModule: true,
  },
  resolve: {
    alias: {
      ...sharedMappings.getAliases(),
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'auth',
      filename: 'authRemoteEntry.js',
      exposes: {
        './RemoteEntryModule': 'apps/auth/src/app/remote-entry/entry.module.ts',
      },
      shared: {
        '@angular/core': { singleton: true, strictVersion: true },
        '@angular/common': { singleton: true, strictVersion: true },
        '@angular/common/http': { singleton: true, strictVersion: true },
        '@angular/router': { singleton: true, strictVersion: true },
        ...sharedMappings.getDescriptors(),
      },
      library: {
        type: 'module',
      },
    }),
    sharedMappings.getPlugin(),
  ],
};

Now we are done with auth application, run nx run auth:serve command to run the application on the dev server


Todo Application

Here we will make the todo interface where users can read, create, update, delete to todos.

First, generate the required Components

Todo List Component

nx g @nrwl/angular:component remote/components/todo-list --project todos --module=entry --inlineTemplate=true --inlineStyle=true

Todo List Item Component

nx g @nrwl/angular:component remote/components/todo-list-item --project todos --module=entry --inlineTemplate=true --inlineStyle=true

Todo Form Component

nx g @nrwl/angular:component remote/components/todo-form --project todos --module=entry --inlineTemplate=true --inlineStyle=true

App Module

  • Then, go to the src/app directory and open app.module.ts file, and match the content below
/*
 * This RemoteEntryModule is imported here to allow TS to find the Module during
 * compilation, allowing it to be included in the built bundle. This is required
 * for the Module Federation Plugin to expose the Module correctly.
 * */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { RemoteEntryModule } from './remote-entry/entry.module';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { HashLocationStrategy, PathLocationStrategy } from '@angular/common';

const routes: Routes = [
  {
    path: '',
    loadChildren: () =>
      import('./remote-entry/entry.module').then((m) => m.RemoteEntryModule),
  },
];

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' }),
  ],
  providers: [
    { provide: PathLocationStrategy, useClass: HashLocationStrategy },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
  • After that, make changes to app.component.ts file with the following
import { Component } from '@angular/core';

@Component({
  selector: 'mfe-demo-prototype-root',
  template: ` <router-outlet></router-outlet> `,
  styles: [],
})
export class AppComponent {}

Remote Entry Module

  • Now go to remote/remote.module.ts file and match the content:
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { TodoFormComponent } from './components/todo-form/todo-form.component';
import { TodoListComponent } from './components/todo-list/todo-list.component';
import { TodoListItemComponent } from './components/todo-list-item/todo-list-item.component';
import { ReactiveFormsModule } from '@angular/forms';

const routes: Routes = [
  {
    path: ':userUid',
    component: TodoListComponent,
  },
];

@NgModule({
  declarations: [TodoFormComponent, TodoListComponent, TodoListItemComponent],
  imports: [
    CommonModule,
    ReactiveFormsModule,
    RouterModule.forChild(routes),
  ],
  providers: [],
})
export class RemoteEntryModule {}

Now its time for components

Todo List Component

go to todo-list.component.ts file and match the content

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Todo, UserTodoService } from '@mfe-demo-prototype/shared/todo';
import { Observable } from 'rxjs';

@Component({
  selector: 'mfe-demo-prototype-todo-list',
  template: `
    <div class="h-full flex-col items-center justify-center ">
      <mfe-demo-prototype-todo-form></mfe-demo-prototype-todo-form>
      <h1 class="text-center text-2xl font-bold mt-5 mb-2">Todos</h1>

      <div
        class="flex flex-col items-center justify-start space-y-5 h-96 flex-grow"
      >
        <mfe-demo-prototype-todo-list-item
          *ngFor="let todo of todos$ | async"
          [data]="todo"
        ></mfe-demo-prototype-todo-list-item>
      </div>
    </div>
  `,
  styles: [],
})
export class TodoListComponent implements OnInit {
  public todos$!: Observable<Todo[]>;

  private userUid!: string;

  constructor(
    private todoService: UserTodoService,
    private activaedRoute: ActivatedRoute
  ) {
    this.activaedRoute.params.subscribe((data) => {
      this.userUid = data['userUid'];
      this.todoService.setUserId(this.userUid);
    });
  }

  ngOnInit(): void {
    this.todos$ = this.todoService.getTodos();
  }
}

Todo List Item Component

import { Component, Input } from '@angular/core';
import { Todo, UserTodoService } from '@mfe-demo-prototype/shared/todo';

@Component({
  selector: 'mfe-demo-prototype-todo-list-item',
  template: `
    <div
      class="p-2 rounded-lg shadow-md bg-gray-100 w-96 flex items-center justify-between"
    >
      <div
        class="flex items-center justify-start space-x-5"
        (click)="updateTodo()"
      >
        <div>
          <!-- icon -->
          <svg
            *ngIf="!data.done"
            xmlns="http://www.w3.org/2000/svg"
            class="h-5 w-5 text-amber-500"
            viewBox="0 0 20 20"
            fill="currentColor"
          >
            <path
              fill-rule="evenodd"
              d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z"
              clip-rule="evenodd"
            />
          </svg>

          <svg
            *ngIf="data.done"
            xmlns="http://www.w3.org/2000/svg"
            class="h-5 w-5 text-green-500"
            viewBox="0 0 20 20"
            fill="currentColor"
          >
            <path
              fill-rule="evenodd"
              d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
              clip-rule="evenodd"
            />
          </svg>
        </div>
        <div>
          {{ data.body }}
        </div>
      </div>
      <button (click)="deleteTodo()">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          class="h-5 w-5 text-red-500"
          viewBox="0 0 20 20"
          fill="currentColor"
        >
          <path
            fill-rule="evenodd"
            d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
            clip-rule="evenodd"
          />
        </svg>
      </button>
    </div>
  `,
  styles: [],
})
export class TodoListItemComponent {
  @Input() data!: Todo;

  constructor(private todoService: UserTodoService) {}

  async updateTodo() {
    await this.todoService.updateTodo(this.data);
  }

  async deleteTodo() {
    await this.todoService.deleteTodo(this.data);
  }
}

Todo Form Component

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserTodoService } from '@mfe-demo-prototype/shared/todo';

@Component({
  selector: 'mfe-demo-prototype-todo-form',
  template: `
    <form
      class="flex items-center justify-center"
      [formGroup]="todoForm"
      (submit)="submit()"
    >
      <div class="flex items-center justify-center shadow-lg rounded-lg">
        <input
          type="text"
          name="body"
          id="body"
          class="rounded-l-lg h-8 p-1 focus:outline-none"
          placeholder="Enter you Todo"
          formControlName="body"
        />
        <button
          type="submit"
          class="rounded-r-lg bg-green-600 h-8 w-8 flex items-center justify-center"
        >
          <svg
            xmlns="http://www.w3.org/2000/svg"
            class="h-6 w-6 text-gray-50"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M12 4v16m8-8H4"
            />
          </svg>
        </button>
      </div>
    </form>
  `,
  styles: [],
})
export class TodoFormComponent implements OnInit {
  public todoForm!: FormGroup;

  constructor(private fb: FormBuilder, private todoService: UserTodoService) {}

  ngOnInit(): void {
    this.todoForm = this.fb.group({
      body: ['', [Validators.required, Validators.minLength(2)]],
    });

    this.todoForm.valueChanges.subscribe(console.log);
  }

  async submit() {
    if (this.todoForm.valid) {
      const body = this.todoForm.get('body')?.value;

      await this.todoService.createTodo(body);

      this.todoForm.patchValue({ body: '' });
    }
  }
}

Webpack Config

With that done now it's time to configure webpack.config.js file

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const path = require('path');

/**
 * We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser
 * builder as it will generate a temporary tsconfig file which contains any required remappings of
 * shared libraries.
 * A remapping will occur when a library is buildable, as webpack needs to know the location of the
 * built files for the buildable library.
 * This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains
 * the location of the generated temporary tsconfig file.
 */
const tsConfigPath =
  process.env.NX_TSCONFIG_PATH ??
  path.join(__dirname, '../../tsconfig.base.json');

const workspaceRootPath = path.join(__dirname, '../../');
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
  tsConfigPath,
  [
    /* mapped paths to share */
    '@mfe-demo-prototype/shared/authentication',
    '@mfe-demo-prototype/shared/todo',
  ],
  workspaceRootPath
);

module.exports = {
  output: {
    uniqueName: 'todos',
    publicPath: 'auto',
  },
  optimization: {
    runtimeChunk: false,
  },
  experiments: {
    outputModule: true,
  },
  resolve: {
    alias: {
      ...sharedMappings.getAliases(),
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'todos',
      filename: 'todosRemoteEntry.js',
      exposes: {
        './RemoteEntryModule':
          'apps/todos/src/app/remote-entry/entry.module.ts',
      },
      shared: {
        '@angular/core': { singleton: true, strictVersion: true },
        '@angular/common': { singleton: true, strictVersion: true },
        '@angular/common/http': { singleton: true, strictVersion: true },
        '@angular/router': { singleton: true, strictVersion: true },
        ...sharedMappings.getDescriptors(),
      },
      library: {
        type: 'module',
      },
    }),
    sharedMappings.getPlugin(),
  ],
};

Now we are all set with Todos Application you can run the dev server and check it yourself. Run nx run todos:serve command.


Dashboard Application

This app will act as a host i.e. this will consume the modules exposed by auth and todo application and also use the shared libraries. It will act as a communicator between auth app and the todo app. It will hold the Authentication State for Route Guarding. The user is not allowed to go to the todos page if he is not authenticated. Let's implement such things quickly. We will quickly generate all the components, directives, etc that we will be needing to accomplish micro frontend architecture.

Generate Route Guards

The guards will protect the todo page from unauthorized access.

  • Signed In Routes will allow routes if users are signed in.
nx g @nrwl/angular:guard guards/signedIn --project dashboard
  • Signed Out will protect users accidentally visiting the login page even after successful login
nx g @nrwl/angular:guard guards/signedOut --project dashboard

Signed In Route Guard

got to guards\signed-in.guard.ts file and match the content:

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  Router,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';
import { map, Observable, } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class SignedInGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router,
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.authService.currentUser().pipe(
      map((user) => !!user),
      map((res) => {
        if (res) {
          return true;
        } else {
          this.router.navigate(['auth','login']);
          return false;
        }
      })
    );
  }
}

Signed Out Guard

got to guards\signed-out.guard.ts file and match the content:

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  Router,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';
import { map, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class SignedOutGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    const userId = this.authService.authUser.value?.uid;
    return this.authService.currentUser().pipe(
      map((user) => !!user),
      map((res) => {
        if (!res) {
          return true;
        }
        this.router.navigate(['dashboard', userId]);
        return false;
      })
    );
  }
}

App Component

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
  AuthService,
  AuthUser,
} from '@mfe-demo-prototype/shared/authentication';
import { UserTodoService } from '@mfe-demo-prototype/shared/todo';
import { Observable } from 'rxjs';

@Component({
  selector: 'mfe-demo-prototype-root',
  template: `
    <div class="min-h-screen bg-gray-300">
      <div class="flex items-center justify-between p-2">
        <h1 class="text-2xl font-bold">
          {{ (user | async)?.name }}'s Dashboard
        </h1>
        <div class="flex items-center justify-end space-x-5">
          <div
            *ngIf="user | async"
            class="flex items-center justify-between space-x-5"
          >
            <a [routerLink]="['dashboard', userId]">Dashboard</a>
            <button (click)="signOut()">Logout</button>
          </div>
          <div
            *ngIf="(user | async) === null"
            class="flex items-center justify-between space-x-5"
          >
            <a routerLink="/auth/login">Login</a>
            <a routerLink="/auth/signup">Signup</a>
          </div>
        </div>
      </div>
      <router-outlet></router-outlet>
    </div>
  `,
  styles: [],
})
export class AppComponent implements OnInit {
  public user!: Observable<AuthUser | null>;
  public userId!: string;

  constructor(
    private authService: AuthService,
    private router: Router,
    private todoService: UserTodoService
  ) {}

  async ngOnInit(): Promise<void> {
    this.user = this.authService.currentUser();
    this.user.subscribe((data) => {
      if (data === null) {
        this.router.navigate(['auth', 'login']);
        // this.router.navigate(['shared']);
      } else {
        this.userId = data.uid;
        this.router.navigate(['dashboard', this.userId]);
      }
    });
  }

  async signOut() {
    await this.authService.signOut();
    await this.todoService.clearTodos();
    await this.router.navigateByUrl('/auth/login');
  }
}

Typescript module declaration

Make a new file decl.d.ts definition file in the src directory to declare auth and todo modules.

//decl.d.ts file
declare module 'auth/RemoteEntryModule';
declare module 'todos/RemoteEntryModule';

App Module

import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { SignedInGuard } from './guards/signed-in.guard';
import { SignedOutGuard } from './guards/signed-out.guard';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';
import { UserTodoService } from '@mfe-demo-prototype/shared/todo';
import { HashLocationStrategy, PathLocationStrategy } from '@angular/common';
import { LoaderDirective } from './core/loader.directive';
import { DynamicLoaderService } from './core/dynamic-loader.service';

const routes: Routes = [
  {
    path: 'auth',
    pathMatch: 'prefix',
    loadChildren: () =>
      import('auth/RemoteEntryModule').then((m) => m.RemoteEntryModule),
    canActivate: [SignedOutGuard],
  },
  {
    path: 'dashboard',
    pathMatch: 'prefix',
    loadChildren: () =>
      import('todos/RemoteEntryModule').then((m) => m.RemoteEntryModule),
    canActivate: [SignedInGuard],
  },
];

@NgModule({
  declarations: [AppComponent, LoaderDirective],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' }),
  ],
  providers: [
    SignedInGuard,
    SignedOutGuard,
    AuthService,
    UserTodoService,
    DynamicLoaderService,
    { provide: PathLocationStrategy, useClass: HashLocationStrategy },
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  bootstrap: [AppComponent],
})
export class AppModule {}

Webpack Config

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const path = require('path');

/**
 * We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser
 * builder as it will generate a temporary tsconfig file which contains any required remappings of
 * shared libraries.
 * A remapping will occur when a library is buildable, as webpack needs to know the location of the
 * built files for the buildable library.
 * This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains
 * the location of the generated temporary tsconfig file.
 */
const tsConfigPath =
  process.env.NX_TSCONFIG_PATH ??
  path.join(__dirname, '../../tsconfig.base.json');

const workspaceRootPath = path.join(__dirname, '../../');
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
  tsConfigPath,
  [
    /* mapped paths to share */
    '@mfe-demo-prototype/shared/authentication',
    '@mfe-demo-prototype/shared/todo',
  ],
  workspaceRootPath
);

module.exports = {
  output: {
    uniqueName: 'dashboard',
    publicPath: 'auto',
  },
  optimization: {
    runtimeChunk: false,
  },
  experiments: {
    outputModule: true,
  },
  resolve: {
    alias: {
      ...sharedMappings.getAliases(),
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      remotes: {
        auth: 'http://localhost:4201/authRemoteEntry.js',
        todos: 'http://localhost:4202/todosRemoteEntry.js',
      },
      shared: {
        '@angular/core': { singleton: true, strictVersion: true },
        '@angular/common': { singleton: true, strictVersion: true },
        '@angular/common/http': { singleton: true, strictVersion: true },
        '@angular/router': { singleton: true, strictVersion: true },
        ...sharedMappings.getDescriptors(),
      },
      library: {
        type: 'module',
      },
    }),
    sharedMappings.getPlugin(),
  ],
};

With that done now we are ready to launch all the apps at once and see how it works.

Run the command nx run-many --parallel --target:serve --all to launch all applications at once or you can also launch individually.

After that open the browser and go to Host application address http://localhost:4200.

0
Subscribe to my newsletter

Read articles from Apedu.co directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Apedu.co
Apedu.co