Angular: Integrating Micro-Frontends using ModuleFederation- Part-III

AngularEnthusiast
JavaScript in Plain English
12 min readMar 23, 2024

--

In this story, we will create a shared library to fetch the runtime environment specific configuration details for the shell and the 2 micro-frontend applications.

Below are the links to the first 2 parts of this series:

Lets begin with the configuration files we have defined for the 3 applications. In this example I have just included the environment details but there can be much more. For instance the URL of the backend service the application needs to connect to and any non-sensitive information.

In all the 3 applications, we have the same structure. It need not be the same but I have kept it same for simplicity. We have created a configurations folder within src/assets with 2 files as you see below.

config-temp.json

{
“env”:”${env}”
}

config.json

{
“env”:”dev”
}

The variables in config-temp.json will substituted at runtime with the target environment details and the contents of the config-temp.json post substitution will be output to the config.json file.

The config.json file just contains the default values, which is useful for local development.

Our objectives:

=> Creating a library and publishing it as an npm package.

=> The library exposes a service and a module. The service contains a Map object. The purpose of the Map object is to store an entry for each application. The entry contains the application name as the key and the value corresponding to the key is the configuration object retrieved from the assets/configurations/config.json file, we discussed in the beginning of the story.

=> We need to first create an entry in the Map object for every application. When and how we do it, will be discussed shortly. Once an entry is created, we just need to fetch the details from the service every time we need it.

=>Thus this library functions as a common configuration store for all the 3 applications.

Lets begin building the shared library.

  1. I have created a workspace named “configuration” with no application using the below command.
ng new configuration — — create-application=false

2. Within this workspace I have created a library named “module-federation-config-lib” using the below command.

ng g library module-federation-config-lib

3. Within the library, there is an autogenerated service, module and component. I have removed the component since there is no need for it.

Our library structure now looks like this:

This is how our service looks like:

I have defined a single interface configModel containing a single property env to keep it simple. It is highly possible that the shell and the micro-frontends have different and multiple configuration details. So you can use a seperate interface for each application.

I have declared a property appConfigurationList which is initialized to an empty Map.

We have defined 2 methods setConfiguration() and getConfiguration() to set data in the Map and to fetch data from the Map respectively.

Lets look at the setConfiguration(). This method accepts 2 arguments. The first argument path is a string containing the path to the file where the configuration object for the application is stored. The 2nd argument appName is also string which contains the application name trying to create an entry in the appConfigurationList Map.

setConfiguration(path:string,appName:string):Observable<configModel>{
return this.http.get(path).pipe(
map((response:any)=>{
this.appConfigurationList.set(appName,response);
return response;
})
)
}

We have used HttpClient to fetch the configuration object from the location specified in the path argument.

The fetched configuration object is stored as the value in the appConfigurationList Map and the argument appName is the key to this value.

So we can expect that the Map contains as many key-value pairs as the no of applications. The key of each entry in the Map is the application name defined in the appName argument and the value corresponding to the key is the configuration object fetched from the location defined in the path argument.

Moving to the getConfiguration(). This method accepts only 1 argument appName which is the application name. As we know that the application name is the key to every entry in the Map. So we use the inbuilt get() of the Map object to extract the value corresponding to the key i.e extract the configuration object corresponding to the application name.

getConfiguration(appName:string){
return this.appConfigurationList.get(appName);
}

4. We have built the library project and published it to npm.

For building the project, we have updated the “build” script in the package.json as below.

“build”: “ng build module-federation-config-lib — configuration=production”,

I update the library version using the below command:

PS C:\Users\User\angular\microfrontends\module-federation-updated\configuration\projects\module-federation-config-lib> npm version 0.0.7
v0.0.7
PS C:\Users\User\angular\microfrontends\module-federation-updated\configuration\projects\module-federation-config-lib>

Next I build the library using the “npm run build” command as below:

PS C:\Users\User\angular\microfrontends\module-federation-updated\configuration> npm run build
> configuration@0.0.0 build
> ng build module-federation-config-lib — configuration=production
Building Angular Package
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Building entry point ‘module-federation-config-lib’
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
✔ Compiling with Angular sources in Ivy partial compilation mode.
✔ Generating FESM bundles
✔ Copying assets
✔ Writing package manifest
✔ Built module-federation-config-lib
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Built Angular Package
— from: C:\Users\User\angular\microfrontends\module-federation-updated\configuration\projects\module-federation-config-lib
— to: C:\Users\User\angular\microfrontends\module-federation-updated\configuration\dist\module-federation-config-lib
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Build at: 2024–03–22T09:37:49.927Z — Time: 2947ms
PS C:\Users\User\angular\microfrontends\module-federation-updated\configuration>

Finally, we are publishing the library to npm using the “npm publish” command. If you have not logged into npm, you will get a prompt to login to npm before publish.

PS C:\Users\User\angular\microfrontends\module-federation-updated\configuration\dist\module-federation-config-lib> npm publish

Below is the link to the npm package and git repository.

5. Next we need to see how we can consume this library in our shell and micro-frontend applications.

We need to follow the same below 2 steps in all the 3 applications:

=> In the package.json, add an additional dependency as below:

“module-federation-config-lib”:”^0.0.7"

=>In the webpack.config.js, update the shared property with the below object. This step we have already done in the first and 2nd part of this series. I mention it again because this is a very important step. It ensures that a single instance of the library is shared between all the 3 application.

“module-federation-config-lib”: {
singleton: true,
strictVersion: true,
requiredVersion: ‘auto’
},

Our shared property finally looks like this:

shared: share({
“@angular/core”: { singleton: true, strictVersion: true, requiredVersion: ‘auto’ },
“@angular/common”: { singleton: true, strictVersion: true, requiredVersion: ‘auto’ },
“@angular/common/http”: { singleton: true, strictVersion: true, requiredVersion: ‘auto’ },
“@angular/router”: { singleton: true, strictVersion: true, requiredVersion: ‘auto’ },
“module-federation-config-lib”: {singleton: true,strictVersion: true,requiredVersion: ‘auto’},
…sharedMappings.getDescriptors()
})

6. Our next step is to use the library to set the configuration details for each application in the Map object within the service exposed by the library.

When do we set the configuration details in the library service for each application ?

For the shell application, we will do this before the application initializes.

For the 2 micro-frontend applications, we have 2 choices.

1. Since we are lazy loading the micro-frontend application on link click, we can also consider setting the configuration details lazily prior to navigating to the route.

OR

2. If you have few micro-frontends, consider setting the configuration details along with the shell application.

Setting configuration details for the shell application

In the shell application, we are setting the configuration details for the application in the library before initialization using the APP_INITIALIZER.

Below is the AppModule for the shell application.

In the AppModule of the shell application, update the providers section with the below object. Please recall that ModuleFederationConfigLibService is defined and exposed from the library we previously created and published to npm.

providers: [ {
provide:APP_INITIALIZER,
useFactory:appInitialization,
deps:[ModuleFederationConfigLibService],
multi:true
},

Define a factory function appInitialization() as below.

function appInitialization(envConfigLibService:ModuleFederationConfigLibService) :()=>Observable<configModel>{
return ()=>envConfigLibService.setConfiguration("assets/configurations/config.json","shell-application")
}

We are calling the setConfiguration() of the ModuleFederationConfigLibService, passing the location of the file containing the configuration object(i.e assets/configurations/config.json) and the application name(i.e shell-application)as arguments. This will create an entry for the shell application in the Map in the ModuleFederationConfigLibService.

Also observe the InjectionToken appName we have created in the AppModule.

export const appName=new InjectionToken(“appName”);

We have registered it with the providers. This is very useful, if you want to access the application name in any other place in the application without any typo mistakes.

{
provide:appName,
useValue:”shell-application”
}

Setting configuration details for the 2 micro-frontend applications

For the 2 micro-frontends, please note that any APP_INITIALIZER defined in the root or feature modules of the application wouldn’t be executed. Hence it is futile to go with that approach. The futility of this approach has motivated us to consider the shared library approach for fetching configuration details.

We have already come up with 2 options earlier: the eager and lazy setting method.

We will see both the approaches.

I. First approach of lazily setting the configuration details

Lets modify the AppRoutingModule in the Shell Application as below:

What have we changed ?

=> We have added a resolve property passing the RemoteConfigurationResolverService as value. This service implements the Resolve interface.

=> We have also added a data property which contains an object holding 2 keys: appName and path. So the key appName contains the name of the application and the key path contains the location of the configuration file.

Below is the RemoteConfigurationResolverService.

These are the 3 observations we make in the above service:

  1. Within the resolve(), we are first checking if an entry already exists for the application name in the Map object within the ModuleFederationConfigLibService using the getConfiguration(). We have passed the application name as argument to the getConfiguration(). If the method returns an object, we know that an entry exists and in that case we simply return the configuration object.
  2. If the method returns undefined, we know that an entry doesn’t exist in the Map object for the application name. So we go ahead and call setConfiguration() of the ModuleFederationConfigLibService, passing the location of configuration file(i.e config.json) for the application and the application name as arguments. A http request is made to fetch the configuration file. The contents of the file are stored as the value corresponding to the key(i.e application name) in the Map object.

3. These arguments(application name and location of configuration file) were provided to the data property in the routing definition as you see below. This means these can be easily accessed in the RemoteConfigurationResolverService via Activatedroute and the same service can be reused for multiple micro-frontends.

data:{appName:”usersApp”,path:”usersApp/assets/configurations/config.json”}
data:{appName:”toDoApp”,path:”toDoApp/assets/configurations/config.json”}

II. Second approach of eagerly setting the configuration details

Here, we don’t need to make any changes in the AppRoutingModule of the shell application and we don’t require any RemoteConfigurationResolverService.

We just need to make a small update in the appInitialization() in the AppModule of the shell application.

The appInitialization() will be updated

From

function appInitialization(envConfigLibService:ModuleFederationConfigLibService) :()=>Observable<configModel>{
return ()=>envConfigLibService.setConfiguration(“assets/configurations/config.json”,”shell-application”)
}

To

function appInitialization(envConfigLibService:ModuleFederationConfigLibService) :()=>Observable<any>{
return ()=>forkJoin([
envConfigLibService.setConfiguration(“assets/configurations/config.json”,”shell-application”),
envConfigLibService.setConfiguration(“toDoApp/assets/configurations/config.json”,”toDoApp”),
envConfigLibService.setConfiguration(“usersApp/assets/configurations/config.json”,”usersApp”)
])
}

We will perform the one-time setting of the configuration details for all the 3 applications.

7. This is the final step where we will see how to access the set configuration details from the library in the micro-frontend and shell application.

I. Lets begin with the Shell Application.

If you recall we are showing the environment details in the header of the shell application as highlighted below:

In the HeaderComponent of the Shell application below, we have referenced the ModuleFederationConfigLibService to call the getConfiguration(), passing the application name as argument.

In the template, we have accessed the config property as below:

<a class=”navbar-brand”>Microfrontends({{config?.env}}) <img src=”assets/microfrontend.png”></a>

II. Moving to the 2 micro-frontend applications.

Incase of Lazy setting approach, you can access the configuration details without referencing the library at all. We have used the ActivatedRoute to access the configuration details returned by the RemoteConfigurationResolverService.

export class ToDoContainerComponent {
constructor(private activeRoute:ActivatedRoute){}
config:Observable<any>|undefined; //for lazy method
//config:configModel|undefined; //for eager method

ngOnInit(){

//this.config=this.envConfigLibService.getConfiguration(this.appName) //for eager method
this.config=this.activeRoute.data; //for lazy method
}
}

In the template, you can use the config property as below to extract the details.

<h4>First MicroFrontEnd Loaded({{(config|async)?.configDetails?.env}})</h4>

Apply the same for the usersApp micro-frontend too.

Incase of Eagerly setting the configuration details, you can reference the ModuleFederationConfigLibService from the library in the constructor and use the getConfiguration() to access the configuration details for the application.

export class ToDoContainerComponent {
constructor(@Inject(appName)public appName:string,private envConfigLibService:ModuleFederationConfigLibService){}
//config:Observable<any>|undefined; //for lazy method

config:configModel|undefined; //for eager method

ngOnInit(){
this.config=this.envConfigLibService.getConfiguration(this.appName) //for eager method
//this.config=this.activeRoute.data; //for lazy method
}
}

In the template, you can use the config property as below to extract the details,

<h4>First MicroFrontEnd Loaded({{config?.env}})</h4>

Apply the same for the usersApp micro-frontend too.

8. Demo

Let me first demonstrate the Eager setting approach.

If I hit localhost:5004 in the browser, this is how my network tab looks like. Please observe 3 Http requests to fetch the config.json file. The 3 applications are setting the configuration details within the Map object in the service exposed by the library.

Clicking on the Users link in the navigation bar,

Let me now demonstrate the Lazy setting approach.

When I hit localhost:5004 in the browser, we see only 1 single http request to fetch the config.json for the shell application. The contents of the fetched config.json file will be used to create an entry in the Map object within the service exposed by the library.

Clicking on the ToDos link in the navigation bar, we see a http request to fetch the config.json file for the toDoApp micro-frontend application.

Clicking on the Users link in the navigation bar, we see a http request to fetch the config.json file for the usersApp micro-frontend application.

Subsequent toggling between the ToDos and Users link in the navigation bar will not make http requests to fetch the config.json file. This is because of the below condition we added in the RemoteConfigurationResolverService. If any entry already exists in the Map object, we just return the value corresponding to the entry in the Map. We do not make a http request to fetch the config.json file.

let configObject=this.envConfigLibService.getConfiguration(route.data[“appName”]);
if(configObject){
//already exists. jsut return the object
return of(configObject);
}

This completes how we are handling runtime configuration information access.

In the final story of this series, we will containerize the 3 applications and deploy them to a Kubernetes cluster

In Plain English 🚀

Thank you for being a part of the In Plain English community! Before you go:

--

--

Loves Angular and Node. I wish to target issues that front end developers struggle with the most.