# Splitting an Angular App into Micro-Frontend Applications: A Step-by-Step Guide
## Introduction to Micro-Frontends
Micro-frontends offer a scalable solution for managing large applications by dividing them into smaller, independent pieces. This approach allows specialized teams to develop, test, and deploy sections of an application separately, improving efficiency and reducing complexity.
### What is a Micro-Frontend?
Micro-frontends apply microservices principles to front-end development, breaking down large applications into manageable parts. These parts, or micro-apps, can be independently developed and integrated, often through routing. For a deeper dive into micro-frontends, especially regarding module federation, we suggest our [article on module federation](https://test.com).
#### Perspectives
Users see a unified application, while developers enjoy flexibility in deployment and serving options, including module federation for integrating micro-apps.
## Micro-Frontend Technology Landscape
Micro-frontends allow using different frameworks or libraries for various application parts, offering flexibility but also posing challenges, such as team mobility. Tools like Single SPA or framework-specific solutions like Nx for Angular help integrate these technologies into a cohesive application. The aim is to independently build, test, and deploy each micro-app while ensuring a seamless user experience.
## Tackling the Transition to Micro-Frontends
### The Challenge of Large Applications
Large applications can suffer from lengthy test and build times, and slow local serving. Transitioning to micro-frontends by splitting the application into smaller parts can significantly alleviate these issues.
There are numerous methods for implementing micro-frontends, with various guides and resources available. Some focus on building micro-frontends from scratch, which is ideal for new projects expected to scale significantly.
## Refactoring an Existing Application
This guide, however, concentrates on refactoring an existing application, potentially built using the Angular CLI, into shared libraries and individual apps. These components are then seamlessly integrated within the larger application, maintaining the unified user experience while enhancing development efficiency.
## Reviewing the Base Application for Micro-Frontend Implementation
### Experiment Setup with a Sample Application
For our exploration into micro-frontends, we will utilize a sample application. This application, while not large or complex, incorporates several services that demonstrate how they can be integrated with each micro-app in a micro-frontend architecture.
#### Application Structure
The application's structure mirrors what one would typically get from creating a new project using the Angular CLI. Key elements include:
- **Feature Modules**: These are specific functionalities or pages within the app, such as 'add-user', 'dashboard', 'login', and 'user-list'.
- **Models**: Data structures that define the shape of data within the app.
- **Shared Services**: Common functionalities used across different parts of the application, such as authentication (`auth`) and user management (`users`).
The directory structure is as follows:
```bash
- src
+ features
- add-user
- dashboard
- login
- user-list
- models
+ shared
- auth
- users
```
### Refactoring Strategy
The goal is to refactor the services and models into libraries that can be shared across micro-apps. The feature modules, on the other hand, will be transformed into their individual apps. This approach ensures modularity and facilitates independent development and deployment of each feature.
#### Routing Considerations
The current routing setup in the application utilizes lazy loading, enhancing performance and user experience. Here's an extract showcasing the routing strategy:
```typescript title="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 {}
```
This routing module demonstrates the use of guards (`isLogged` and `isNotLogged`) to manage access to different routes based on user authentication status, and dynamic imports for lazy loading of feature modules.
### Starting with Simple Steps in Micro-Frontend Migration
#### Approach to Migrating a Complex Application
Migrating a complex application to a micro-frontend architecture can be challenging. To facilitate this process, clear steps and possibly additional tooling (for better dependency management) are essential.
##### Initial Focus: Dependency-Free Files
1. **Identify Independent Files**: Start by pinpointing files that do not have dependencies but are used across services and feature modules. For example, in our sample application, this applies to the `models` folder, which contains models used throughout the application but doesn't depend on any other files.
2. **Create a New Library for these Files**: Use Angular CLI to generate a library that will house these independent files. The command for this is:
```bash
ng generate library models
```
This command results in the following actions:
- Creation of a new `projects` folder containing the `models` library.
- Installation of `ng-packagr` as a dependency.
- Updates to `angular.json` and `tsconfig.json` to include the new library.
#### Customizing the Library
- **Path Mapping**: Modify the path mapping in `tsconfig.json` to avoid potential conflicts with npm modules. A suggested prefix is `@@`, as npm module names cannot include double at signs.
- **Package Naming**: If you intend to publish the library to a registry, ensure the name in `package.json` is registry-safe. Otherwise, you can choose a convenient name.
#### Refactoring the Models
1. **Move the Models**: Transfer the contents of the `models` folder into the new library. The generated library will contain default components, modules, and services, which can be removed. Place the user model file inside the `lib` folder of the library.
2. **Update Public API**: Modify the `public-api.ts` file to export the appropriate content from the `lib` folder.
#### Managing File Movement
- **Using Git for File Transfers**: Employ `git mv` instead of operating system features for moving files. This ensures better tracking of changes by Git.
#### Library Folder Structure
The folder structure of the `models` library should be organized for clarity and easy access.
#### Updating Imports
**Change Import Paths**: Replace existing import paths with the new library path. For instance, change:
```javascript
// from
import { User } from 'src/app/models/user';
// to
import { User } from '@@models';
```
Ensure that the path matches what is specified in `tsconfig.json`.
#### Building the Library
**Resolving Errors**: Initially, you may encounter errors due to the library not being built. Resolve this by running:
```bash
ng build models
```
This command compiles the `models` library, allowing TypeScript to correctly reference `@@models`.
#### Advancing Beyond Basic Migration
After successfully migrating independent code into libraries, the next step involves tackling dependent code that can also be isolated. Continuing with our example, we'll focus on migrating the `auth` service, including its guards, into a separate library. This process mirrors the steps taken for the `models` library.
#### Creating the Auth Library
### Generate the Library
Use Angular CLI to create the `auth` library with the command:
```bash
ng generate library auth
```
### Refactor the Library Content:
Remove the default content from the `lib` folder and transfer all contents of the `auth` folder into it. Update the `public-api.ts` file to reflect these changes.
The `public-api.ts` should look something like this:
```javascript
/*
* Public API Surface of auth
*/
export * from './lib/auth.service';
export * from './lib/is-logged.guard';
export * from './lib/is-not-logged.guard';
```
### Build the Library
Compile the `auth` library using Angular CLI:
```bash
ng build auth
```
### Update Imports
Change the application's import statements to use the new `auth` library.
Congratulations are in order! Parts of your application have been successfully migrated into libraries. This not only keeps the application running but potentially speeds it up, as Angular doesn't need to rebuild the entire application. You now have several libraries with isolated testing and building, suitable for larger applications where components, services, pipes, and other elements are shared.
#### Advanced Library Organization
For more complex applications, consider using ng-packagr's Secondary Entrypoints feature to group common items like components or services. This requires additional configuration adjustments.
### Migrating the First Application
Having learned to package libraries, we now turn to migrating a feature module into its own application.
#### Choosing a Module for Migration
In our example, the `Login` feature module is selected for migration. Ensure that all shared artifacts used by this module are already in their respective libraries. Other services, like the `users` service, can be migrated later.
#### Creating the Login Application
### Generate the Application
Use Angular CLI to create the `login` application:
```bash
ng generate application login --style=scss --routing
```
The `--routing` flag is essential for navigation within the app.
### Serve the Application
Test the newly created application with:
```bash
ng serve login
```
If the main application is running, you may need to use a different port.
### Refactoring the Feature Module
1. **Transfer the Feature Module**: Instead of replacing files as done with libraries, create a `feature` folder in the `src` of the new application and move the `login` folder into it.
2. **Update Routing**: Modify the `app-routing.module.ts` of the `login` application to serve the `Login` module at the root route.
3. **Adjust the AppComponent**: Ensure the `app.component.html` of the `login` app contains a `<router-outlet></router-outlet>` tag for routing to function correctly.
### Addressing Styling
Initially, the new application might appear unstyled. Copy the `styles.scss` file from the main application to the new one to resolve this. Future posts will delve into sharing styles across applications.
### Outcome
If done correctly, the `login` application should now be running independently, styled, and functional, marking a significant step in your journey towards a fully implemented micro-frontend architecture.
## Integrating the Login App into the Main Application
After setting up the `Login` application as a standalone entity, the next critical step is integrating it with the main application. This involves building the `Login` app as a library and importing it using the existing lazy load feature in the main module.
### Updating `angular.json`
1. **Modify Project Configuration**: In `angular.json`, duplicate a library configuration, placing it under the `login` configuration for better organization. Rename the key to `login-lib` and update references (such as `root`, `sourceRoot`) to point to the `login` application.
-
The updated configuration should be adjusted to reflect the specific paths and settings of the `login` application.
\+
2\. **Creating `ng-package.json`**: This file is essential for instructing `ng-packagr` on how to package the application. Copy an existing `ng-package.json` file from another library into the root folder of the `Login` application. Update the `dest` key to `"../../dist/login-lib"` to specify the build output directory.
### Establishing the Public API
1. **Create `public-api.ts`**: This file, different from typical library setups, should be created under the `src` folder. It should export only the `Login` feature module:
```javascript
// public-api.ts
export { LoginModule } from './app/feature/login/login.module';
```
### Building the Library
1. **Prepare `package.json`**: Copy a `package.json` from another library to the root of the `Login` application, ensuring to update the project name.
2. **Build Command**: Execute the Angular CLI build command:
```bash
ng build login-lib
```
This process compiles the `Login` module as a library.
### Importing the Library
1. **Update `tsconfig.json`**: Add a new path mapping for the `login-lib` in `tsconfig.json`, pointing to `"dist/login-lib"`. This step is crucial for TypeScript to locate the library correctly.
2. **Modify Application Routing**: In `app-routing.module.ts`, update the lazy loading path to use the new library:
```javascript
loadChildren: () => import('@@login').then((m) => m.LoginModule),
```
### Running the Integrated Application
With these changes, you can serve the application as usual. The `Login` module, now a separate project, should function seamlessly within the main app.
### Further Opportunities and Considerations
- **Automating Builds**: To streamline the process, consider using tools like Lerna for automated build and dependency management. This can help especially when dealing with multiple libraries and applications.
- **Version Management**: Currently, the setup does not utilize versioning for libraries, which could complicate the introduction of breaking changes. Implementing a versioning strategy could be beneficial for long-term maintenance.