DEV Community

Cover image for How to split an Angular app into micro-frontend apps
Michael De Abreu
Michael De Abreu

Posted on • Updated on

How to split an Angular app into micro-frontend apps

Hi. It's been a while. I hope you are doing great these days. Today, I want to show you something I've been playing with recently.

In this modern world, applications seem to be growing in all directions, and then you need this larger team to deal with your big app, and eventually, you found that you have several smaller teams, working only on a section of the application each of them, yet they all need to download, run, build, and test, the whole application. If you are in this position, the micro-frontend approach might suit your desire to scale better.

What's a micro-frontend?

I don't want to focus this post on what a micro-frontend is, but I will try to introduce this concept briefly. By now we all know microservices, that is instead of having a large monolithic application serving all your requests, we divided it into services, allowing each service to be developed, tested, built, and delivered individually. Micro-frontend is the same concept but applied to front-end applications. This approach allows us to create smaller applications, that can be developed, tested, and built individually and will be integrated as part of a larger application, probably using routing to serve each application.

From the user's point of view, they will be using one application. From the development point of view, it depends on how you want to serve the applications. You could serve them individually, package them all as one application using advanced techniques like module federation, or use other techniques like the one I am about to present.

For this example, we will be ending up building one application, that will import all other applications, but each application can be locally served, tested, and deployed, individually.

Current micro-frontend state of art

Since micro-frontend allows us to develop applications individually, this also means you can use different frameworks and libraries together to develop each section of this application. Of course, this will have some advantages and disadvantages. If you are using different technologies to serve different parts of the application, it won't be so easy to move developers within teams. However, the door is open, and if you want to do something like that, you could use something like Single SPA, or some other, to help you organize and connect the different technologies into a larger application.

You can also use just one frontend tool, like React, Vue, or in this case, Angular. Each of them has some way to achieve what we need. If you prefer doing in this way, one tool you could use is Nx. It has some really cool features such as depency graph, linked package, and others. It also has first-class support for Module Federation and Micro-frontend development.

In the end, what we want is to able to build, test, and deploy each application on its own, and to serve all of them to the user like they are using just one app.

How to split the hydra?

So, you may have this big application, and you are noticing that tests are taking really long to finish, and building takes even longer. Even serving locally the app takes what seems forever. And you had read something about micro-frontends and spitting the app into mini-apps can help you reduce all of this. You are right.

I know that I've said it before, but I'll say it again: You have many alternatives to implementing micro-frontend. For sure you are going to find other guides describing other methods, or maybe even some others with the same method I'm using.

The ones that I've found describe how you should use the micro-frontend approach from scratch, and surely, if you are starting a project and you know ahead is going to be big enough to need this architecture, maybe you could check those first and see if that is something that fits want you need better.

But this is not about creating an app from scratch using a micro-frontend, this is about refactoring an existing application, that you might have already created using Angular CLI, into shared libraries, and individual apps, that can be integrated within the same larger app seamlessly.

Review the base application

We are going to use this app for the experiment. You might notice is not really a big app, that's because it's not even a real app. But you might also notice that the application uses a couple of services that will allow us to see how these services will integrate with each application.

The basic structure of the application is the same one you get from creating a new project with Angular CLI. Within the application, we have a couple of services, models, and feature modules. The goal of this will be to refactor the services and models into libraries that can be shared, and the feature modules into their individual apps.

- src
  + features
    - add-user
    - dashboard
    - login
    - user-list
  - models
  + shared
    - auth
    - users
Enter fullscreen mode Exit fullscreen mode

The routing currently uses lazy loading, as you can see in the following code extract:

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { isLogged } from './shared/auth/is-logged.guard';
import { isNotLogged } from './shared/auth/is-not-logged.guard';

const routes: Routes = [
  {
    path: '',
    canMatch: [isLogged],
    loadChildren: () =>
      import('./features/dashboard/dashboard.module').then((m) => m.DashboardModule),
  },
  {
    path: '',
    canMatch: [isNotLogged],
    loadChildren: () => import('./features/login/login.module').then((m) => m.LoginModule),
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
Enter fullscreen mode Exit fullscreen mode

You can find the source code that we are going to use in this repo

Start easy

Migrating a complex application won't be easy, and I'll try to provide some clear pathways that you can use to do it. However, it would help if you considered that you might need additional tooling, especially to identify better the dependencies between your files.

Because first we are going to identify some files that don't have dependencies, but with files that depend on them, create a new library and move those files to that library. In this case, we have the models folder, containing the only model used in the application, and that model doesn't depend on any other file, but is used across services and feature modules.

Now, to create the library, we are going to use the following Angular CLI command

ng generate library models

This will create a new folder named projects, and inside that folder, our library will be created. Additionally, this will install the ng-packagr as a root dependency, update the angular.json file to include the models library project description, and update the tsconfig.json file with a path mapping to the built version of the library. By default, the path will be the name of the library, but I would recommend updating it to something less likely to conflict with an actual npm module. I'm using @@ as a prefix here because you can't use double at sign as an npm module name.

Additionally, you could also update the name in the package.json file of the library. You might get a warning, but because this is not going to be published on a register you should be fine. However, if you want to publish it on a register you should use a register-safe name then.

Having the library created, we are going to move the models folder content into this new library. You might notice that this library will create a component, a module, and a service for you by default, we can remove all of them, and then place the user model file inside the lib folder of that library. Update the public-api.ts file to export what should be the public content of the lib folder. In the end, you should have something like this:

Folder structure of the models library

To move files around you might use the git mv instead of the OS features to allow git to track better the changes. Also, you can review the additional files generated by this command if you want, but we will play with them later anyway.

After that, we are going to update the imports to use this library. So, instead of using this as:

import { User } from 'src/app/models/user';
Enter fullscreen mode Exit fullscreen mode

We are going to update to:

import { User } from '@@models';
Enter fullscreen mode Exit fullscreen mode

Remember to use the path you have in your tsconfig.json file.

By now you should have some errors because we haven't built the library yet. So, if we run:

ng build models
Enter fullscreen mode Exit fullscreen mode

The Angular CLI will build the models library, and now TS will properly get your @@models references.

We are halfway there

Well, not really, but I think this is getting long. Anyway, after you have migrated all your non-depended code into a library, you can start identifying some dependent code that can be isolated into libraries as well. To continue with this example, we are going to migrate now one of the services, specifically the auth folder, including the guards. This will be the same as the models, create the library, move the folder content into the lib folder, build, and update the references.

Run the Angular CLI command to create the auth library.

ng generate library auth

Delete the content of the lib folder and move all the auth folder content into the lib folder. Finally, update the public-api.ts file. You should have something like this:

Auth library folder structure

And your public-api.ts file should have something like this:

/*
 * Public API Surface of auth
 */

export * from './lib/auth.service';
export * from './lib/is-logged.guard';
export * from './lib/is-not-logged.guard';
Enter fullscreen mode Exit fullscreen mode

Run the Angular CLI command to build the auth library.

ng build auth

You can now update the imports of the auth service and guards to use the library.

Congrats!

You have migrated some parts of your application into libraries. And if you followed it correctly, the application is still running! Probably even slightly faster, because Angular doesn't need to build the whole application to serve it. Now you have several libraries, in our case two, with isolated testing, and building.

In this example, the application is sharing only a model and a couple of services, but in a larger application, you could be sharing components, services, pipes, and other pieces.

Additionally, to create a new library for each piece you want to share, you could use the Secondary entrypoints feature of ng-packagr to group common things together, like components or services, and group them together by a more specific feature. But consider this will requires additional updates to the configuration.

Migrating the first app

So far, we have seen how to migrate simple parts of the code into libraries. This will allow us to reuse them between our apps, but we are also learning how to package a library.

To migrate the first app, I will suggest that at least you have all shared artifacts the selected app is using in their own libraries. You might notice that we are still having the users service. But since the module we are going to migrate is not using this service, we can safely migrate that service later.

Don't worry if things don't work the first time. A migration like this will take time. I'm only showing you how I could do it right, but not the times I failed. Remember, Git is your best friend in this case, create as many branches as you need.

The first thing we need to do is to identify the feature module we are going to migrate. For this example, if you had reviewed the code, you might already guess, we are going to migrate the Login feature module.

We can create the application using the Angular CLI command:

ng generate application login --style=scss --routing

The --style=scss is optional, you can use the style you want, but the --routing is strongly suggested, you will see why shortly.

With this, we will have a new login application within the projects folder, and the angular.json updated with the application. To try the recently created application, you need to run the Angular CLI command:

ng serve login

If you are running the main application, you will see this message:

? Port 4200 is already in use.
Would you like to use a different port?

You could select yes, and a random port will be used, update the angular.json file or use the --port option in the CLI, to have a fixed port instead.

We are going to move the feature module into the application, but instead of replacing the existing files as we did with the libraries, we are going to create a feature folder in the src folder of the new application and move the login folder into it. You should have something like this:

Login application folder structure

We are going to update the login app to serve this feature module with the root route.

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    loadChildren: () => import('./feature/login/login.module').then((m) => m.LoginModule),
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
Enter fullscreen mode Exit fullscreen mode
<!-- app.component.html -->
<router-outlet></router-outlet>
Enter fullscreen mode Exit fullscreen mode

If you did everything correctly, you should have now an unstyled application. That's because the original application was using Material, and we need to set up Material SCSS for this application as well. For now, we will only copy and paste the styles.scss file from the main app into this one. In a later post, we will see how to share styles between apps.

After this, you should have the application running and looking good.

Integrating the Login app within the main app

We have the individual application up and running, however, we still need some way to integrate it with the main app. What we are going to do to do so, is to build the application and import it using the lazy load feature the main module is using already. However, we are going to build the application as a library.

To set this up, we need to manually create a project configuration in the angular.json file and some files that will be used by ng-packagr. Luckily for us, we have some libraries already that we can use as a template for those that we need to create.

Updating the angular.json

Find a library configuration, and copy and paste all of it just under the login configuration, for them somewhat to be grouped. Change the name (the key) of the configuration to login-lib. Update the root, sourceRoot, and others that reference the original library with references to the login application. Usually what you need to do is to rename the original folder name of the library with login, but for the typescript config files, you could create them, or use the ones you have (The applications are created with one less file than the libraries, for some reason).

The configuration should look something like this:

Image description

As you can see, we don't have the ng-package.json file, so let's going to create it.

Create the ng-package.json file

The ng-package.json file describes how ng-packagr should package the application. It doesn't have any relation to a regular package.json file.

We are going to copy the ng-package.json file from one of the existing libraries and paste it into the login folder, the root folder of the Login application. If you open the file, you can notice three keys: $schema, dest, and lib. The first is used to help some editors to verify the schema of the JSON file, the second is used to define where the library will be built, and the last is an object to configure the library, including the entryFile. We are going to update the dest value to "dest": "../../dist/login-lib". This will tell ng-packagr to build our application as a library in that folder. The content of the ng-package.json should be something like this:

Image description

Again, you might notice that we don't have the public-api.ts file.

Creating the public-api.ts file

With this one, we don't need to copy and paste, because this will be different from the ones libraries normally use.

Create a file under the src folder and name it public-api.ts. Open it and export just the Login feature module.

// public-api.ts
export { LoginModule } from './app/feature/login/login.module';
Enter fullscreen mode Exit fullscreen mode

Building the library

You might have guessed by now, but the goal here is to create a library with the feature module as the only thing being exported. To do that, ng-packagr needs a package.json as well in the Login application root folder. You can copy one from another library but be sure to update the name of the project.

Now run the build Angular CLI command

ng build login-lib

And you should have a newly built library feature module.

Importing the library

All those files that we needed to create, are created by us when we use the ng generate library command. Additionally, it also updates the tsconfig.json file to path map to the built version of those libraries. We are going to do exactly that.

Open the tsconfig.json, and in the paths object, add another key with the name that you will be using for the library, and point the path to "dist/login-lib". Your path configuration should look something like this:

Image description

If you are using other names to map your paths, is ok, just make sure that you are pointing correctly to the built version of each library.

After you have updated your tsconfig.json, the only thing you need to do is update the app-routing.module.ts.

From this:

loadChildren: () => import('./features/login/login.module').then((m) => m.LoginModule),
Enter fullscreen mode Exit fullscreen mode

We are going to update to:

loadChildren: () => import('@@login').then((m) => m.LoginModule),
Enter fullscreen mode Exit fullscreen mode

After this, you should be able to serve the application as usual.

You have your first application migrated away!

Awesome! If you follow along, you can now safely serve the application, and it will work as before. But internally, you have now separated projects for each slice of your application, where you can run separate commands, reducing the scope of each, and improving the time each take to run each command.

Opportunities

With the current code, each time we clone the code, or update a library, including one of the apps, we would need to manually run the build command. But, because they are not being package together, we won't need to do this for their dependents, all of them will work assuming we didn't break anything. Additionally, this also means we are not using versions for handling each library, so it could become difficult to introduce a breaking change.

We could improve part of this by using something like Lerna. With the right configuration, Lerna can be really helpful.

That's all folks!

As usual, thank you for reading. I really appreciate the time you all take to read my articles. We are more than 5000 now, so thank you all. I really hope you can find this useful. I don't think another post to migrate the other features modules are necessary, but I'll try to do one about sharing styles, or maybe something about configuring the secondary endpoints of the ng-packagr. I could also do a post about configuring Lerna to work with this and improve the dependency building. But I think it would be better if you let me know in the comments what would be more interesting for you.

Photo by Christian Holzinger on Unsplash

Top comments (12)

Collapse
 
wgbn profile image
Walter Gandarella

I confess that I was a little frustrated with the end of the article, because without using module fereration this is simply not a micro-frontend!

What he did was basically create libraries (including the login app could easily have been a common library, because it is possible to export modules with their own routes inside a lib).
In the end, it's all still part of a single build/deploy.

I agree that the code is more organized, but this is precisely the concept of libraries, not micro-frontend.

A micro-frontend is expected to be independent even in its deployment. App A is deployed on host A, app B is deployed on host B, however, app B can be called and integrated into app A through module federation, including sharing state. The big advantage is that changes and deploys of app B are made without changing anything or deploying in app A.

Collapse
 
michaeljota profile image
Michael De Abreu

Hey! Thanks for the suggestion, I think you will find my newest post useful if you want to add Module Federation to this.

Collapse
 
shacharharshuv profile image
Shachar Har-Shuv

Sadly, this was the first result when I googled "Angular micro front-end"

Collapse
 
michaeljota profile image
Michael De Abreu

Thanks! I wrote another post talking about how to add module federation to this series.

Collapse
 
coturiv profile image
coturiv

Exactly, he is talking about the Angular library, and needs more knowledge about the Micro frontend.

Collapse
 
michaeljota profile image
Michael De Abreu

Hi! Sorry for the delay. You are right, this is the first step to migrate the app over to a micro-frontend approach and I didn't get to the actual micro-frontend. However, you will find the follow-up post useful if you want to add module federation to this project.

Collapse
 
pbouillon profile image
Pierre Bouillon

Thanks a lot for this article! Micro frontends is a concept that I find hard to get started but with this step by step explanation it sure is a lot clearer

Collapse
 
quedicesebas profile image
Sebastián Rojas Ricaurte

Waiting for the style sharing party :)... And what about state?

Collapse
 
michaeljota profile image
Michael De Abreu

You can see in this example we are sharing the Auth Service between the apps. By using a self registered service at root the state of the service will be shared. I'll give a comment when the shared styles is ready. :)

Collapse
 
helloooojoe profile image
Joseph Garza

Using module federation, how would you create a microfrontend in Angular app A and load it into legacy app B's HTML? I went about using Angular Elements to creator a custom web element.

Collapse
 
michaeljota profile image
Michael De Abreu

I guess it depends on what exactly you want to end with. Angular Elements can help you sharing components and that, but I guess sharing state is where things can get tricky. Someone else asked about integrating something like this with Meteor, that would behave the same as your legacy HTML app. Maybe I create a post about it. I'll let you know.

Collapse
 
vonsko profile image
vonsko

tbh I ditched builtin angular libraries about two years ago in favor nx. it's way much better in problem solving in decentralized structure