How to split an Angular app into micro-frontend apps
Tags: angular, tutorial, architecture
Published: 2023-03-28 03:16:17

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](https://single-spa.js.org/), 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](https://nx.dev/). 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](https://nx.dev/recipes/module-federation).

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
```

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

```ts
// 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 {}
```

> You can find the source code that we are going to use [in this repo](https://github.com/michaeljota/angular-microfrontend)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/efineq61i5wjdde3bn3x.png)

> 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: 

```ts
import { User } from 'src/app/models/user';
```

We are going to update to: 

```ts
import { User } from '@@models';
```

> 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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e9und6lv1qq9xlrnloj3.png)

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

```ts
/*
 * Public API Surface of auth
 */

export * from './lib/auth.service';
export * from './lib/is-logged.guard';
export * from './lib/is-not-logged.guard';
```

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](https://github.com/ng-packagr/ng-packagr/blob/main/docs/secondary-entrypoints.md) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/10iilhv4rkq5y7mn23bq.png)

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

```ts
// 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 {}
```

```html
<!-- app.component.html -->
<router-outlet></router-outlet>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3vthvf19z8gol90iacqj.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j91myw0aekusip537kfd.png)

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.

```ts
// public-api.ts
export { LoginModule } from './app/feature/login/login.module';
```

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w36r1a1cjxz3dna9sd6o.png)

> 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:

```ts
loadChildren: () => import('./features/login/login.module').then((m) => m.LoginModule),
```

We are going to update to: 

```ts
loadChildren: () => import('@@login').then((m) => m.LoginModule),
```

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](https://lerna.js.org). 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](https://unsplash.com/@pixelatelier?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/photos/NK06SbuCbio?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)