As soon as web applications grow bigger, they become difficult to manage. So lets just follow the main idea of engineering, which is breaking the problem into smaller problems. So extending the concepts of micro-services to the frontend, we can break a huge application on the basis of routes to mini apps or MVP's (Minimum Viable Product).
When should I choose
We need to choose this architecture wisely as it comes with some shortcomings as well. So, if you are working on a huge web app, and you need a quicker deliverables, which are independent to each other, the trade off would be worth it.
Why choose Micro-frontends/MVP
This concept of route based application breakup can help out ship mini-apps faster, eliminating the risk of breaking the previously deployed MVP's.
The idea behind micro-frontends is to divide the web app to multiple features as independent products, whose ownership can be divided amongst independent teams. The idea allows each team to function across different area of business.
The idea have a lot of tech pros:
- Single Responsibility principle/ Business Centric
- Faster and Parallel shipping of products/features
- Technology Agnostic (One can run on React while other is free to choose Vue or Angular)
- Resilient - If one mini-app has a only that mini-app needs to go down or redeployed.
- Sharing of static assets
The Repo Approach
As we are dealing with multi-apps, we need to make a choice of mono-repo or a multi-repo approach. It is not difficult to guess that handling one repo would far easier than handling multiple repos. So in case your mini-apps are using the same library/framework its suggested to take the Mono-repo approach. By this approach, the same repo would be accustomed to ship out multiple builds based on the routes you want to bind with a MVP.
However, if you want to try out multiple techs or want to separate out code you would be forced to use multi-repo.
One of the great way to manage your mono-repos is this
Architecture Design Principles
So the design principle says that the each microfrontend should :
- Follow the Single responsibility Principle of the SOLID rules
- Be Business centric
- Autonomous Feature
- Frontend Framework Agnostic
- Super Resilient
How to Implement
Without taking more time, lets deep dive on how to break the application into micro frontends.
First of all, we need to categorise our mini applications. So for that, lets create a file named as miniapp.js having the below contents:
const MINIAPPS = {
minapp1: {
id: 'app1',
patterns: ['/minapp1'],
subPath: '/mainSubpath/minapp1'
},
minapp2: {
id: 'app2',
patterns: ['/minapp2'],
subPath: '/mainSubpath/minapp2'
},
minapp3: {
id: 'app3',
patterns: ['/minapp3'],
subPath: '/mainSubpath/minapp3'
},
};
module.exports = MINIAPPS;
So, to explain the above, we are planning to break our web app into 3 micro-frontends, with each having a specific subpath. So in short we are aiming to build 3 MVP's, each having a separate build, with the below paths:
www.mywebapplication.com/mainsubPath/minapp1
www.mywebapplication.com/mainsubPath/minapp2
www.mywebapplication.com/mainsubPath/minapp3
The main mainSubPath is added for the extendability of multiple webapps inside www.mywebapplication.com. In case its not your requirement you can keep the subPath property to be '/miniapp1'
So in order to read the routes from the next.config, lets create a file which has the all the routes from the application on the root of the application preferably.
routes-list
const APP_ROUTES = [
{
name: 'Feature 1',
page: 'miniapp1/feature1',
pattern: '/miniapp1/feature1'
},
{
name: 'Feature 2',
page: 'miniapp1/feature2',
pattern: '/miniapp2/my-route/feature2'
},
{
name: 'Feature 3',
page: 'miniapp2/feature3',
pattern: '/miniapp2/feature3'
},
{
name: 'Feature 4',
page: 'miniapp2/feature4',
pattern: '/miniapp2/my-route/my-sub-route/feature4'
},
{
name: 'Feature 5',
page: 'miniapp3/feature5',
pattern: '/miniapp3/feature5/my-feature'
},
{
name: 'Feature 6',
page: 'miniapp3/feature6',
pattern: '/miniapp3/my-route/my-subroute/feature4'
}
];
module.exports = APP_ROUTES;
Just keep in mind, that while creating the features, create folders with names feature1 having index.js file rather than having a feature1.js in pages folder.
Now, we just need to write some minor logic in next.config.js in order to read only specific routes needed for each miniapp.
next.config.js
In the top of the file we need to add:
const MINIAPPS = require('./miniapp');
const APP_ROUTES = require('./routes-list');
const miniappToBeBuild = process.env.APP_NAME;
const basePath = __dirname;
const subDir = NODE_ENV === 'production' ? (miniappToBeBuild ? MINIAPPS[miniappToBeBuild].subPath : '/mainsubPath') : '';
if (miniappToBeBuild && MINIAPPS[miniappToBeBuild]) {
console.log('MINIPP NAME ---> ', process.env.APP_NAME);
console.log('MINIPP Subpath ---> ', MINIAPPS[process.env.APP_NAME].subPath);
}
const getExportPaths = () => APP_ROUTES.filter((appRoute) => {
const filterFlag = MINIAPPS[miniappToBeBuild].patterns.filter((appPattern) => appRoute.pattern.indexOf(appPattern) === 0);
return filterFlag.length > 0;
});
process.env.SUB_DIR = subDir;
and in the module.exports section we need to add the below code.
module.exports = {
assetPrefix: subDir,
async exportPathMap() {
const paths = {};
let dynamicSection = '';
let exportRoutes = APP_ROUTES;
if (miniappToBeBuild && MINIAPPS[miniappToBeBuild]) {
console.log(`Building miniapp-${miniappToBeBuild} with subpath-${MINIAPPS[miniappToBeBuild].subPath}`);
exportRoutes = getExportPaths();
}
exportRoutes.forEach((routes) => {
paths[routes.pattern] = { page: routes.pattern };
});
return paths;
},
generateBuildId: async () => version,
webpack: (config, { isServer }) => {
return config;
}
};
So basically, the exportPathMap function is provided by next js incase you don't want to read your routes from the pages folder and want to have a custom logic for reading the routes. So In that function we added the logic that whatever APP_NAME we pass, only the routes starting with that APP_NAME gets created in the build. (One minor drawback of this approach is that rather than just the required all js files get created, but that's not harming at all as none of them is linked in the scripts of required HTML's)
At last, we just need to write scripts, to pass the APP_NAME and to create separate builds for each miniapp.
Something like this: -
package.json
"scripts": {
"export:minapp1": "npm run clean && cross-env APP_NAME=minapp1 npm run build && cross-env APP_NAME=minapp1 next export && shx mv out/minapp1/* out/ && shx rm -r out/minapp1",
"export:minapp2": "npm run clean && cross-env APP_NAME=minapp2 npm run build && cross-env APP_NAME=minapp2 next export && shx mv out/minapp2/* out/ && shx rm -r out/minapp2",
"export:minapp3": "npm run clean && cross-env APP_NAME=minapp3 npm run build && cross-env APP_NAME=minapp3 next export && shx mv out/minapp3/* out/ && shx rm -r out/minapp3",
"dev": "npm run build && next dev",
"build": "NODE_ENV=production next build",
"clean": "rimraf node_modules/.cache .next",
}
To run the above commands on both mac and Windows 2 libraries were required. So pre-requisite: npm i cross-env shx
So thats all folks, By adding the above minor snippets your micro frontend is ready. In case you want to add another tech miniapp, just create a new repo with that tech and in the build system update the subpath from '' to 'mainSubpath/miniapp4'.
In later posts, I'll show how I created CD/CD pipelines using Azure devops.
Conclusion
Micro frontends is a better architectural approach if you have a huge repo, getting difficult to manage and faster deliveries are required, which are supposed to be independent for each other.
Thats all folks !!
Top comments (8)
Thanks for the article. Is there any repository that I can check the code?
Or should I simply create a nextjs app and put those files and codes inside and run it?
Am I missing something or are we just creating separate pages?
I don't have any repo published for this, but yeah i can surely create one I a day or two.
And yes we are basically creating separate pages and breaking the app to Micro-frontends on these route based pages, if I understand you correctly.
But then if we change something in an app the whole application must be built. Isn't that something we should avoid?
In case you change anything in your application, you just need to build the miniapp which has the code for that particular route. Eventually that's the beauty of Micro-frontends that you need to build and deploy only selected code. So that you can feel assured on nothing breaks on other apps.
interesting, any update about the repository?
Nice article. What if theres' a need to have shared components among the mini apps.
Thanks Rakesh. Actually the idea is keeping a single codebase for all the miniapps. So kinda everything is shared, just built separately or rather built segregatedly.
Where can I download the code?