Angular and Microfrontend Pipelines
A lot of people use the microfrontend these days. I’m sure you know the reasons, so I won’t dwell on it. This article is very technical.
Here, I’ll cover some tricks on how to make a (relatively) seamless integration between an Angular host app and a React-based microfrontend. This is a fairly brief overview without a deep dive, almost no theory, and with very simplified — illustrative — subsystems, but the approach is real, tested and used in the wild.
TLDR: how to build, serve and test Angular application alongside a React microfrontend with a single
Under the Hood
- Angular Host.
- React-based micro-frontend.
- Jest for Unit Tests.
- Playwright for end-to-end tests.
Let’s speak about the Microfrontend first.
Here I use an approach that has proven itself over the years of practice and is applied in different projects. There are several basic principles.
Second, there is a special module, Loader, which dynamically loads that bundles in runtime to the Host application, and mounts their parts on demand to the Containers.
This is a slightly different approach than the module federation — micro-application packages are simply placed in the main application’s output folder and the Loader makes requests to them, for example
However, such micro-applications are easy to serve in the same way Webpack module federation does, with HTTP and separate URL/port.
Third, there are same shared dependencies which are not included into micro-applications and must be provided as an external, vendor packages. E.g., React or React-DOM.
There are no separate repositories for each part. There are no NPM packages for Loader, UI Kit and Services — in fact, there is no UI Kit and Services at all.
No container builds. No build consolidation and shared configs and build scripts.
No runtime configuration, no runtime application register (dynamic per-user UI management).
Very simple feature set, it is just a skeleton with little to no generalisation or configuration, lots of hardcoded values, names and paths. Almost no Typescript interfaces, generic types and aliases.
The purpose of this article is to show you how to integrate with Angular, not how to create full microfrontends and hybrid architectures. However, this approach can be scaled up to production in stages.
The functional foundation of a microfrontend (how micro-applications are built) should follow these principles:
- Be lightweight, otherwise it makes little sense to use.
- Allow dynamic mounting and unmounting (very important remark) to an external container — unlike AngularJS.
- Allow to be instantiated multiple times (no global scope pollution and conflicts).
- Allow to use static dependencies, aka loading the runtime from CDN or from pre-built packages.
- Not being a framework which has its own opinion regarding app lifecycle, routing, change detection, patching of global objects — like Angular.
- Be able to be isolated: provide separate rebuilds, containerised pipelines, independent versioning, testing and deployment. This is not about the engine itself but about the ecosystem, but anyway, it must be mentioned.
That’s why React. Well, it could actually be Vue, or Svelte, or vanilla JS, whatever — any “library” — but React is very famous, has many components, and a large community.
Micro-applications and Views
Each micro-application can — not must– provide one or more Views. View is a basic named piece which can be addressed by the Loader mount function. E.g., micro-application Users may include views UsersList, UserCard, AdminProfile, etc.
Non-visual micro-applications are usually Services and are not covered by this example, as well as an UI Kit package. These topics are really big and sophisticated, and include data management, design systems, local- and global stores, local- and global message buses, dependency resolving, etc.
Host Container component
A special host app component (i.e. existing in the Angular code) that provides a) a DOM container, usually a div, that a particular micro-application should occupy, and b) calls the loader to mount the micro app on initialisation and unmount it when the container is destroyed.
In addition, the container component is often responsible for passing data in and out of the micro-application — sharing data and logic between the host application and the micro-applications, dynamic loading — displaying a loading indicator during loading and initialisation of the micro-application, etc.
Micro-frontend Loader must be, umm, loaded before first usage of the microapp in a complex hybrid application. Then — static dependencies. Then — if exist — Services and UI Library. This is a bootstrapping phase.
But where are the micro-applications? They will be loaded later by the Loader and Host Container Components. To be clear, vice versa: Container requests the particular micro-application to be loaded by Loader, and, then, provides a
div where requested View must be mounted.
- Host application provides a Container,
- Container requests some App/View, like
- Loader resolves and loads the app bundle, both JS and CSS,
- Loader/mount instantiates and attaches the View requested to the Container,
- Everything lives together.
Building and Testing
Each part of microfrontend architecture must be independent, but somehow orchestrated to work together.
It means, each subsystem has to –
- Be able to be isolated in a separate Git repository,
- Have own dependencies (
- Have standardised build interface,
- Have standardised output folder,
- Be able to be an NPM package (optional, not covered here),
- Provide Dockerized build-, test-, develop pipelines (is not covered here).
Therefore, the loader, and micro-applications, and any other possible parts must support such commands and contracts:
npm run buildfor production build,
npm run devfor development build,
npm run testto run unit tests,
npm run e2eto run e2e tests,
./distas the output folder.
There are a lot of nuances in building full-fledged solutions, most of them are related to the establishment of contracts, rules and automated tools: how to build everything uniformly, how to bring it all together and manage it all — not even how to use the micro-frontend architecture, but how to organise and maintain its parts . As with microservices, the question is not how they work, but how they work together.
These topics are very important but are not covered in this article. Just a minimal set of requirements, no physical separation, no advanced control over the structure of the source code.
Well, it’s time to start.
1. Packages and dependencies
A very important topic in the microfrontend is dependency management. Not only because if you have independent
node_modules in each micro-application, you will easily end up with tens of gigabytes of duplicate packages. But because you may end up with an incompatible set of underlying (vendor) dependencies, such as different versions of React.
Also, we need to somehow centralise the installation process. Running
npm install for each nested folder may be very inconvenient.
We will use
workspaces from NPM.
Let’s add the workspaces we have in the
microfrontend folder (into main
The second step is aligning dependencies. Let’s remove all
node_modules folders in the project, including nested, if any, and run
npm install. It will collect the dependencies from all the workspaces PLUS Angular, dedupe, install packages and create symlinks to nested folders. It means, there will be a single
node_modules instead of a bunch of local packages — great economy.
Please pay attention: in the nested
package.json files field
name must be unique and (this is not mandatory but a good practice) be the same as the containing folder:
In the current setup, the
name is also the name of micro-application. The Host Container will demand this application as
app-simple as well — for more sophisticated configurations you can use advanced naming, but here the name of folder = name of workspace = name of micro-application = names of the bundle files (
The third, optional step, is projecting NPM commands from microfrontend parts:
With that you’ll be able to run independent build or test of all the micro-application in one turn. However, if you want more granularity, you can add more specific commands:
to be able to build, e.g., the Loader.
Well, hereby we are able to build the Angular App with
ng build, and all the microfrontend with
npm run microfrontend-build.
Now we need to automate the process because it is inconvenient. We need to make such things automatic:
- Building Angular,
- Building every part of microfrontend,
- Copying build artefacts from each microfrontend part to Angular’s output
- Extend Angular’s
index.htmlfile with microfrontend: add loading the Loader, add vendors packages.
2. Custom builder configuration
We need to run
npm run build in each microfrontend folder, or just execute
npm run microfrontend-build we’ve created on the previous step. But auto-build is not optimal because then we need to individually copy build artefacts to output
assets folder anyway.
So, I‘ve created a Webpack plugin which will run on every rebuild and put the results into current compilation assets.
Interesting: the approach can be adapted to work with React, or Vue, or any other Webpack-based pipeline. Just add the plugin(s) manually.
The most simple way to alter Angular’s pipeline is using a custom builder. I use this project to attach to the build pipeline.
Let’s install the dependency with
npm i -D @angular-builders/custom-webpack. Then edit
angular.json file to call our custom Webpack extension:
Angular’s implicit Webpack config does not allow multi-configurations, otherwise we would just return an array of Webpack-configs and everything would work auto-magically.
Our microfrontend is built upon ESBuild, not Webpack, and anyway — pipelines must be isolated. Perhaps, some of your micro-applications built with Vue + Vite, or Rollup: that’s why using of
npm run is better than sharing a Webpack config.
All the our Webpack config does is adding a couple of plugins to the process:
2.1. Index.html transformation
Angular Host knows nothing about the needs of the microfrontend. We need to manually add the
loader.js script loading, and static dependencies to
index.html. Static deps (React, React-DOM) will be added from CDN for simplicity.
I added a simple transformer,
microfrontend-html-transform.js, which adds dependencies to the end of
- Make vendor’s dependencies to use local copies, i.e.,
node_modules/react/umd/react.production.min.jsinstead of CDN — copy the file to
assetsand write corresponding path to the
- Add a consistency check, like runtime versions tests for each micro-application, version of the Loader, UI Kit, Services. You need to add some fields to package.json files and process them on transformation.
- Add web-worker to support the hot reloading in dev mode.
3. Building and Serving
Here we have two plugins:
Our Webpack file,
microfrontend-webpack.config.js, detects the environment and mode and adds the plugins to the config. First plugin does all the work whereas the second one just adds files to the watch list in
serve mode (
The serve mode implementation is pretty straightforward. Plugin just scans all the files by
microfrontend/**/src/* pattern and adds them to the dependencies of the current compilation. As a result, if you change any of the files — the build process is restarted for each part of the microfrontend. This, of course, is not an optimal solution, but a full recovery takes about 2 seconds and is tolerable.
Even worse, there is no automatic host application reload and/or module hot-swap — it’s up to you. In the current configuration, the developer needs to press F5 or Ctrl/Cmd-R to reload the page after rebuilding the micro-application. In addition, when a file added, build must be manually restarted because scanning is performed only at startup.
Microfrontend Loader can include its own listener or just a manual ‘re-mount’ button to reload a particular micro-application on demand, or automatically based on websocket notification.
However, in full-scaled application configuration there should be a detection of a project changed, partial rebuild (and redeploy) and some reload approach, or at least configurable ‘watch’ mode when developer can specify what micro-applications must be watched.
There are a lot of things that can be improved here:
- source code maps in dev mode,
- better watch, including additions/removals, change tracking, hot reloading, reporting of build errors in the browser,
- better reporting, like file sizes, total files processed, etc.
- ESLint support and automatic unit testing on production builds.
Well, testing is a big topic with a lot of options available. Here, in this example, two types of tests are available: Unit testing with
npm run test and end-to-end testing with
npm run e2e.
The second kind of testing is based on the
npm run start command which compiles per-project mini-application (see
src/tests/e2e/index.e2e.tsx). To do that, a special builder configuration,
esbuild.e2e.js, builds and serves it on port 8000.
In this article I will cover only Unit Tests pipelines integration.
The most simple approach is adding a launcher to
MicrofrontendKarma() function executes all the
npm run test commands for each piece of microfrontend, and intercepts the output with
Then, with pretty simple parser it extracts the build stat (‘FAIL’, total tests number) and provides a summary fake test suite — special spec where each
it(…) entry is the name of microfrontend part tested.
This way, the developer can see the results of the unit tests in one run of the
ng test :
Of course, the approach is very basic and a lot of features can be added. E.g. Webpack plugins (need to use
karma-webpack) to automatically re-run tests on file change — like it is implemented for build, one plugin to run tests, another one to watch the files.
Also, better logs integration to see a full report in Jasmine. Also, merge coverage reports. A SonarQube analyzer. E2E reports consolidation.
That’s all. In this article, I’ve described a hybrid app, an Angular-based host, and a React-based microfrontend, and how parts can be consistent during build and test processes.
Each micro-application here can be built, tested — with both Unit tests and e2e tests — and independently developed with
All together can be installed with
npm install, built with
ng build, tested with
ng test in a single run.