Build and Deploy the Remote Applications to Netlify

Colum Ferry - Jan 21 '22 - - Dev Community

This is the third article in a series of articles that aims to showcase the process of scaffolding and deploying a Micro Frontend Architecture using Nx and Netlify. We will build and deploy the remote applications. We’ll build a login app and a todo app and deploy each independently to Netlify.


Follow us on Twitter or subscribe to the newsletter to get notified when new articles get published.


Overview

In this article, we will build two applications that we will deploy separately to their own sites. We will configure them as Remote Micro Frontend Applications, exposing certain code via the Module Federation Plugin for webpack. This exposed code can then be consumed by our Dashboard application from the deployed location of the remote applications.

We will build a ToDo app, which will be non-functional and whose sole purpose is to be a placeholder to protect behind an authorization guard. It will contain a simple UI.

We will also build a Login app, which will provide a basic login form along with a shared auth lib containing a stateful service for managing the authed user.

Build ToDo App

Generate the app

Starting with the ToDo app, run the following command to generate the app with a Micro Frontend configuration.

yarn nx g @nrwl/angular:app todo --mfe --mfeType=remote --host=dashboard --port=4201 --routing=true
Enter fullscreen mode Exit fullscreen mode

Let’s break down what is happening with this command.

  • It generates a standard Angular app with a routing configuration.
  • It adds an Angular Module that acts as a remote entry point for host applications.
  • It adds a webpack configuration exposing the Remote Entry Module to be consumed by Host applications.
  • It will add this application to the specified host application’s (dashboard) webpack configuration.
  • It adds this application to the host application’s serve-mfe target.
  • This target will serve all the remote applications along with the host application, launching your full Micro Frontend Architecture.
  • It changes the default serve port for the application to 4201.

Build the UI

Now we’ll build out the UI for the ToDo application. We’ll start by adding a route that will redirect automatically to the Remote Entry Module. This means that when we serve the ToDo app locally, we’ll see the Module that we’re working on for the MFE.

Open apps/todo/src/app/app.module.ts and find the RouterModule import in the NgModule. It should look like this:

RouterModule.forRoot([], { initialNavigation: 'enabledBlocking' }),
Enter fullscreen mode Exit fullscreen mode

Edit it to match the following:

RouterModule.forRoot(
      [
        {
          path: '',
          loadChildren: () =>
            import('./remote-entry/entry.module').then(
              (m) => m.RemoteEntryModule
            ),
        },
      ],
      { initialNavigation: 'enabledBlocking' }
    ),
Enter fullscreen mode Exit fullscreen mode

Next, we’ll edit the app.component.html file to only contain the RouterOutlet. Open the file and delete all the contents except for

<router-outlet></router-outlet>
Enter fullscreen mode Exit fullscreen mode

If we serve our app using yarn nx serve todo and navigate to http://localhost:4201 we should see the following:

Our ToDo app has been configured correctly. Let’s edit the entry.component.ts file to show a very basic ToDo UI:

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

@Component({
  selector: 'mfe-netlify-todo-entry',
  template: `<div class="todo-list">
    <h1>Todo</h1>
    <div class="list">
      <label> <input type="checkbox" name="item" /> Item </label>
    </div>
  </div> `,
})
export class RemoteEntryComponent {}
Enter fullscreen mode Exit fullscreen mode

When we save the file, webpack should rebuild the changes and our output should look like this:

That’s it. The UI for our ToDo app is complete.

Prepare for Netlify Deployment

We have one final step before we are ready to deploy the app. We need to add a netlify.toml file to the src/ folder of the ToDo app.
After creating the file, add the following to it:

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[[headers]]
  # Define which paths this specific [[headers]] block will cover.
  for = "/*"

  [headers.values]
    Access-Control-Allow-Origin = "*"
Enter fullscreen mode Exit fullscreen mode

To ensure, this file is copied correctly when the file is built, open up the project.json file for your ToDo app (apps/todo/project.json) and find the build option. It should look like this:

    "build": {
      "executor": "@nrwl/angular:webpack-browser",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/todo",
        "index": "apps/todo/src/index.html",
        "main": "apps/todo/src/main.ts",
        "polyfills": "apps/todo/src/polyfills.ts",
        "tsConfig": "apps/todo/tsconfig.app.json",
        "inlineStyleLanguage": "scss",
        "assets": [
          "apps/todo/src/favicon.ico",
          "apps/todo/src/assets"
        ],
        "styles": ["apps/todo/src/styles.scss"],
        "scripts": [],
        "customWebpackConfig": {
          "path": "apps/todo/webpack.config.js"
        }
      },
Enter fullscreen mode Exit fullscreen mode

Add the netlify.toml file to the assets array so that it gets copied over in place. Your build config should look like this:

    "build": {
      "executor": "@nrwl/angular:webpack-browser",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/todo",
        "index": "apps/todo/src/index.html",
        "main": "apps/todo/src/main.ts",
        "polyfills": "apps/todo/src/polyfills.ts",
        "tsConfig": "apps/todo/tsconfig.app.json",
        "inlineStyleLanguage": "scss",
        "assets": [
          "apps/todo/src/favicon.ico",
          "apps/todo/src/assets",
          "apps/todo/src/netlify.toml"
        ],
        "styles": ["apps/todo/src/styles.scss"],
        "scripts": [],
        "customWebpackConfig": {
          "path": "apps/todo/webpack.config.js"
        }
      },
Enter fullscreen mode Exit fullscreen mode

Let’s commit our changes and push to our remote repo:

git add .
git commit -m “feat: build the todo application”
git push
Enter fullscreen mode Exit fullscreen mode

Now the application is ready to be deployed to Netlify!

Deploy the ToDo App

Let’s deploy our ToDo app to Netlify. Go to https://app.netlify.com.
You’ll be greeted with a screen similar to this, if you are logged in:

Image description

To set up our ToDo site, follow the steps below:
You can see a gif of this here

  • Click on Add new site
  • Click on GitHub when it prompts to Connect to Git provider.
  • Select your repository
  • Modify the Build command and Publish directory
    • Build command should be yarn build todo
    • Publish directory should be dist/apps/todo
  • Click Deploy site

Netlify will then import your repository and run the build command. After the build completes, Netlify will take the built files and deploy them to a newly generated domain. You can find this domain in the Info card on the Netlify Site. Clicking on the URL will take you to your deployed application.

With that, our ToDo app is complete!

Build the Login App

Moving on to the Login app. Here, we will build a few things:
A Shared Auth Library that can be used by any app or library in our Micro Frontend Architecture.
A Login library that will contain a login form and use the Auth library to set the authenticated user state.
The Login app, which will use the Login library to render the login form.

Scaffold the Application and Libraries

We’ll start by scaffolding the app and the libraries we’ll need:

yarn nx g @nrwl/angular:app login --mfe --mfeType=remote --host=dashboard --port=4202 --routing=true
yarn nx g @nrwl/angular:lib feat-login 
yarn nx g @nrwl/angular:lib shared/auth
Enter fullscreen mode Exit fullscreen mode

Add Shared Auth Logic

Now that we have our libraries ready, let’s flesh out the logic for the shared auth library. We’re going to want two things:

  1. A service that will log the user in and contain some state about the authed user
  2. A route guard that can be used to check if there is an authenticated user

We can use generators to scaffold these out also! Run the following commands to do so:

yarn nx g @nrwl/angular:service auth --project=shared-auth
yarn nx g @nrwl/angular:guard auth --project=shared-auth --implements=CanActivate
Enter fullscreen mode Exit fullscreen mode

These two commands have added four files to our shared/auth library:

  • libs/shared/auth/src/lib/auth.service.ts
  • libs/shared/auth/src/lib/auth.service.spec.ts
  • libs/shared/auth/src/lib/auth.guard.ts
  • libs/shared/auth/src/lib/auth.guard.spec.ts

For convenience, we’ll ignore the test files.
We’ll start with the auth.service.ts file. Open the file and replace its contents with the following:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private _activeUser = new BehaviorSubject<{ username: string } | undefined>(
    undefined
  );

  activeUser = this._activeUser.asObservable();

  login({ username, password }: { username: string; password: string }) {
    if (password === 'password') {
      this._activeUser.next({ username });
      return true;
    }
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

In this file, we’re doing the following:

  • Creating a BehaviorSubject to store some state relating to our User
  • Exposing an observable that can be used to read the current state of the User
  • Exposing a very trustworthy method to log the User in and set the state

Next, we’ll build the Auth Guard logic to prevent unwanted routing to protected routes. Open auth.guard.ts and replace the contents with the following:

import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { map, tap, Observable } from 'rxjs';
import { AuthService } from './auth.service';

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

  canActivate():
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.authService.activeUser.pipe(
      map((activeUser) => Boolean(activeUser)),
      tap((isLoggedIn) => {
        if (!isLoggedIn) {
          this.router.navigateByUrl('login');
        }
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In this file, we use the Auth Service we created to read the state of the authed user, map it to a boolean value that will be used as the result of the guard. We also create a side-effect that will force navigation to the login route if the user is not authenticated.

Finally, we need to expose both the guard and the service as exports from the library to allow them to be consumed by other libraries and applications. Open libs/shared/auth/src/index.ts and replace the contents with:

export * from './lib/auth.guard';
export * from './lib/auth.service';
Enter fullscreen mode Exit fullscreen mode

With that, our shared auth library is ready to be used!

Build the Login form

Now that we have the shared auth library completed, we can focus on building the login form. We already generated the login feature (feat-login) library. This approach is an architectural practice promoted by Nrwl to help structure your monorepo logically. You can read more about that here: https://go.nrwl.io/angular-enterprise-monorepo-patterns-new-book

We need a component for our login form, so let’s generate one:

yarn nx g @nrwl/angular:component login --project=feat-login
Enter fullscreen mode Exit fullscreen mode

First, open libs/feat-login/src/lib/feat-login.module.ts and add LoginComponent to the exports of the NgModule and ReactiveFormsModule to the imports array:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { LoginComponent } from './login/login.component';

@NgModule({
  imports: [CommonModule, ReactiveFormsModule],
  declarations: [LoginComponent],
  exports: [LoginComponent],
})
export class FeatLoginModule {}
Enter fullscreen mode Exit fullscreen mode

This allows consuming libraries and apps to import the module and use the component easily.

Next, we’ll build the login form itself.
Open login.component.ts and replace it with the following:

import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '@mfe-netlify/shared/auth';

@Component({
  selector: 'mfe-netlify-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'],
})
export class LoginComponent {
  loginForm = new FormGroup({
    username: new FormControl('', [Validators.required]),
    password: new FormControl('', [Validators.required]),
  });

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

  login() {
    const username = this.loginForm.get('username')?.value;
    const password = this.loginForm.get('password')?.value;
    const loggedIn = this.authService.login({ username, password });

    if (loggedIn) {
      this.router.navigateByUrl('/');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With this component, we create a FormGroup that will be used to collect user input. It also has a method for handling the submission of the login form that will use our Auth Service to authenticate the user, and route us back to the root of the application, where we should now see the previously protected content.

With the logic taken care of, let’s flesh out the UI.
Open login.component.html and replace it with:

<div class="login-form">
  <form [formGroup]="loginForm" (ngSubmit)="login()">
    <input
      type="text"
      name="username"
      placeholder="username"
      formControlName="username"
    />
    <input
      type="password"
      name="password"
      placeholder="password"
      formControlName="password"
    />
    <button type="submit">Login</button>
  </form>
</div>
Enter fullscreen mode Exit fullscreen mode

Finally, let’s add some CSS so it looks pretty. Open login.component.scss and add:

.login-form {
  padding: 1.5em;
  display: flex;
  flex-direction: column;
  align-items: center;
}
form {
  display: flex;
  flex-direction: column;
  align-items: center;
}
input {
  margin: 0.5em 0;
  padding: 0.5em;
  border: 1px solid grey;
  border-radius: 4px;
}
button {
  padding: 1em;
  appearance: none;
  border: 1px solid rgb(99, 99, 214);
  background-color: rgb(47, 72, 143);
  border-radius: 4px;
  text-transform: uppercase;
  color: white;
  cursor: pointer;
}

button:active {
  background-color: rgb(86, 106, 160);
}
Enter fullscreen mode Exit fullscreen mode

With that, the login form should be ready to be used!

Integrate the Login form to the Login app

With the login form completed, it’s time to use it in the login application we generated earlier. Following similar steps as the ToDo application, let’s set up the routing to point to the Remote Entry Module.

Open apps/login/src/app/app.module.ts and find the RouterModule import in the NgModule. It should look like this:

RouterModule.forRoot([], { initialNavigation: 'enabledBlocking' }),
Enter fullscreen mode Exit fullscreen mode

Edit it to match the following:

RouterModule.forRoot(
      [
        {
          path: '',
          loadChildren: () =>
            import('./remote-entry/entry.module').then(
              (m) => m.RemoteEntryModule
            ),
        },
      ],
      { initialNavigation: 'enabledBlocking' }
    ),
Enter fullscreen mode Exit fullscreen mode

Next, we’ll edit the app.component.html file to only contain the RouterOutlet. Open the file and delete all the contents except for

<router-outlet></router-outlet>
Enter fullscreen mode Exit fullscreen mode

Now, let’s edit the Remote Entry component to use our login form. First we need to import it to the Remote Entry Module, so let’s open entry.module.ts and replace it with:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';

import { RemoteEntryComponent } from './entry.component';
import { FeatLoginModule } from '@mfe-netlify/feat-login';

@NgModule({
  declarations: [RemoteEntryComponent],
  imports: [
    FeatLoginModule,
    CommonModule,
    RouterModule.forChild([
      {
        path: '',
        component: RemoteEntryComponent,
      },
    ]),
  ],
  providers: [],
})
export class RemoteEntryModule {}
Enter fullscreen mode Exit fullscreen mode

Now, let’s edit the RemoteEntryComponent to render our Login form. Open entry.component.html and replace it with:

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

@Component({
  selector: 'mfe-netlify-login-entry',
  template: `<mfe-netlify-login></mfe-netlify-login>`,
})
export class RemoteEntryComponent {}
Enter fullscreen mode Exit fullscreen mode

Our Login app should be ready!
If we run yarn nx serve login and navigate to http://localhost:4202 we should see the following:

Image description

Awesome! We just need to add our netlify.toml file and we should be ready to deploy our Login app to Netlify! We’ll follow the same steps we used to create the file for the ToDo app.

Prepare for Netlify Deployment

We need to add the netlify.toml file to the src/ folder of the Login app.
After creating the file, add the following to it:

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[[headers]]
  # Define which paths this specific [[headers]] block will cover.
  for = "/*"

  [headers.values]
    Access-Control-Allow-Origin = "*"
Enter fullscreen mode Exit fullscreen mode

To ensure, this file is copied correctly when the file is built, open up the project.json file for your Login app (apps/login/project.json) and find the build option. It should look like this:

    "build": {
      "executor": "@nrwl/angular:webpack-browser",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/login",
        "index": "apps/login/src/index.html",
        "main": "apps/login/src/main.ts",
        "polyfills": "apps/login/src/polyfills.ts",
        "tsConfig": "apps/login/tsconfig.app.json",
        "inlineStyleLanguage": "scss",
        "assets": [
          "apps/login/src/favicon.ico",
          "apps/login/src/assets"
        ],
        "styles": ["apps/login/src/styles.scss"],
        "scripts": [],
        "customWebpackConfig": {
          "path": "apps/login/webpack.config.js"
        }
      },
Enter fullscreen mode Exit fullscreen mode

Add the netlify.toml file to the assets array so that it gets copied over in place. Your build config should look like this:

    "build": {
      "executor": "@nrwl/angular:webpack-browser",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/login/todo",
        "index": "apps/login/src/index.html",
        "main": "apps/login/src/main.ts",
        "polyfills": "apps/login/src/polyfills.ts",
        "tsConfig": "apps/login/tsconfig.app.json",
        "inlineStyleLanguage": "scss",
        "assets": [
          "apps/login/src/favicon.ico",
          "apps/login/src/assets",
          "apps/login/src/netlify.toml"
        ],
        "styles": ["apps/login/src/styles.scss"],
        "scripts": [],
        "customWebpackConfig": {
          "path": "apps/login/webpack.config.js"
        }
      },
Enter fullscreen mode Exit fullscreen mode

Let’s commit our changes and push to our remote repo:

git add .
git commit -m “feat: build the login application”
git push
Enter fullscreen mode Exit fullscreen mode

Now the application is ready to be deployed to Netlify!

Deploy the Login App

To deploy the Login app, we’ll follow the same steps we used to deploy the ToDo app.

  1. Go to https://app.netlify.com.
  2. Click on Add new site
  3. Click on GitHub when it prompts to Connect to Git provider.
  4. Select your repository
  5. Modify the Build command and Publish directory.
    • Build command should be yarn build login.
    • Publish directory should be dist/apps/login.
  6. Click Deploy site

Netlify will build your app then take the built files and deploy them to a newly generated domain. You can find this domain in the Info card on the Netlify Site. Clicking on the URL will take you to your deployed application.

With that, our Login app is complete!

Summary

In this article, we built and deployed our two remote applications! This sets us up for the next article where we will use Module Federation with our Dashboard application to remotely fetch the exposed modules from our remote apps and compose them into a single system.

Blog: https://blog.nrwl.io/
NxDevTools’ Twitter: https://twitter.com/NxDevTools
Nrwl’s Twitter: https://twitter.com/nrwl_io
Colum Ferry’s Twitter: https://twitter.com/FerryColum

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .