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
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' }),
Edit it to match the following:
RouterModule.forRoot(
[
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.module').then(
(m) => m.RemoteEntryModule
),
},
],
{ initialNavigation: 'enabledBlocking' }
),
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>
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 {}
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 = "*"
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"
}
},
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"
}
},
Let’s commit our changes and push to our remote repo:
git add .
git commit -m “feat: build the todo application”
git push
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:
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
- Build command should be
- 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
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:
- A service that will log the user in and contain some state about the authed user
- 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
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;
}
}
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');
}
})
);
}
}
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';
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
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 {}
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('/');
}
}
}
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>
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);
}
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' }),
Edit it to match the following:
RouterModule.forRoot(
[
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.module').then(
(m) => m.RemoteEntryModule
),
},
],
{ initialNavigation: 'enabledBlocking' }
),
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>
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 {}
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 {}
Our Login app should be ready!
If we run yarn nx serve login
and navigate to http://localhost:4202 we should see the following:
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 = "*"
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"
}
},
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"
}
},
Let’s commit our changes and push to our remote repo:
git add .
git commit -m “feat: build the login application”
git push
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.
- Go to https://app.netlify.com.
- 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 login
. - Publish directory should be
dist/apps/login
.
- Build command should be
- 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