Making an Angular project mono repo with NgRx state management and lazy-loading.

By using Angular CLI, NgRx feature modules and a bit of manual work:-)

Making an Angular project mono repo with NgRx state management and lazy-loading.
Prerequisites: you should be familiar with the NgRx Store management system and Angular CLI.

Goal

Once upon a time, I was asked to implement an Angular 7 CLI monorepo starter with the following project structure:

  1. We have one CoreApp
  2. We have 2 sub-applications (App1 and App2).
  3. App1 and App2 can be lazy-loaded as routes in CoreApp. (Also App1 and App2 can be started and developed independently but with some overhead)
  4. Each of App1 and App2 uses NgRx state management, so you can use Redux-Dev-Tools to observe their Stores.
  5. We have a shared components library Admin-lib. It can be built and used in any of apps (CoreApp, App1 or App2)
  6. Everything should be done with Angular CLI 7.2.x

A simple project diagram looks like this:

Creating project main structure with Angular CLI

Angular CLI 7 supports creating the main application (code is located in srcfolder) as well as the sub-applications and libraries (these are located in the project folder of main repo).

Here are the Angular CLI commands to create a project according to our diagram:

npm install -g @angular/cli    # in case you don't have it installed
ng new CoreApp --routing=true  # creates main CoreApp with routing
ng generate application app1   # creates app1 applciation 
ng generate application app2          # creates app2 applciation
ng generate lib admin-lib     # creates admin-lib component library

After all these manipulations our project folder structure should look like this:

Project folder structure

For now, all these parts exist separately, so our goal is to make them work together. But first, we should at least create commands to be able to run CoreApp, App1 or App2 in dev mode, build them, run their respective unit tests, and lint.

Adding start, build, test and lint commands to package.json for each application

If we take a look at package.json scripts section, we can see the following commands there:

"scripts": {
  "ng": "ng",
  "start": "ng serve",
  "build": "ng build",   
  "test": "ng test",
  "lint": "ng lint",
  "e2e": "ng e2e"
},

All of these commands only work for the main CoreApp application.

You can run them with npm start or npm run build (or any other command) in a terminal. All script commands except start should be executed like:
npm run <commandName>

In the scripts section, you can add your own commands for other applications or libraries.

"scripts": {
  "ng": "ng",
  "start:Core": "ng serve --port 4444",  #start CoreApp in dev mode*
  "start:app1": "ng serve app1 --port 4422", #start App1 in dev mode
  "start:app2": "ng serve app2 --port 4423", #start App2 in dev mode
  "build:core": "ng build --prod --stats-json", #build CoreApp
  "build:admin-lib": "ng build admin-lib", #build admin-lib library
  "build:app1": "ng build app1", # build App1
  "build:app2": "ng build app2", # build App2
  "test:core": "ng test",  # run jasmine unit tests for CoreApp
  "test:app1": "ng test app1", # run jasmine unit tests for App1
  "test:app2": "ng test app2", # run jasmine unit tests for App2
  "lint:core": "ng lint",  # run lint unit tests for CoreApp
  "lint:app1": "ng lint app1", # run lint unit tests for App1
  "lint:app2": "ng lint app2", # run lint unit tests for App2
  "e2e": "ng e2e"
},

*All the text after # should be removed when you place it in package.json

Now we can start any application with the respective command! For example:

npm run start:Core

Or

npm run start:app1

After configuring the main scripts in package.json our TODO steps will be:

  • Add some component to admin-lib, build admin-lib and use it in our applications (App1 and App2).
  • Configure routing and NgRx in App1 and App2
  • Configure /app1 and /app2 routes in CoreApp routing module to be able to run App1 and App2 inside CoreApp as lazy-loaded routing modules.
  • Configure typescript in CoreApp src/tsconfig.app.json to be able to compile code from App1 and App2 inside CoreApp.

Building admin-lib components

OK, let's add admin-lib-component with a component which we gonna use in other applications:

admin-lib.component.ts
admin-lib.component.ts

And add this component to admin-lib.module.ts.

Once we generated the admin-lib library, Angular CLI added these lines in the main project tsconfig.json:

Check the source code of tsconfig.json here.

This means that we should build our admin lib first, and then we will be able to import it like:

import {AdminLibModule} from 'admin-lib';

To build admin-lib we should run:

npm run build:admin-lib

it is just one of the commands we have in our package.json file:

build:admin-lib

After running it dist/admin-lib folder will appear. Now we can import admin-lib.module in any other application.

dist/admin-lib

NgRx is heavily using RxJS under the hood. Want to pump up this skill? Packtpub.com and I prepared a whole RxJS review course with many other details of how you can solve your every-day developer tasks with this amazing library. Take a look!


App1 and App2 configuration

Our agenda is:

  • Configuring routing
  • Adding NgRx with its respective feature modules
  • Adding the component from the admin-lib library

Adding routing

Let's create our first component in App1 and make it the default for our routing. We will add the admin-lib module (we built it in the previous section) in App1 app.module as well.

routing config in App1 app.module.ts

Adding NgRx with its respective feature modules

I prefer adding the NgRx stuff with NgRx Schematics. But diving into NgRx Schematics is beyond the scope of this article. You can read about this wonderful tool here. And in this article by Wes Grimes, you can find a very good NgRx file structure for enterprise Angular applications.

I’ve added a simple boolean switcher that invert its values on each action hit. To implement this I create a store folder in the app1/src/app directory and add hide-show.reducer.ts, hide-show.selectors.ts, and hide-show.actions.ts files.

Adding NgRx to App1

Now our App1 store has this structure:

{
  app1ShowHide: boolean;
}

Let's use admin-lib-component (from the library) in the App1 first component template. And by switching app1ShowHide in the Store we will show or hide this component in App1.

Now let's start the App1 application and check out how it works:

Starting App1

Still don't have ReduxDevTools Chrome plugin installed to observe ngRxStore activities?:-O Here it is:)

Now we can do the same with App2: add NgRx feature branch, add Secondcomponent, subscribe the component to Store and show/hide admin-lib-component in its template.

Configuring App2

Configuring routes in CoreApp

Our next step is to make App1 and App2 work as lazy-loaded modules of CoreApp router. So let's add respective routes into app-routing.module.ts of main CoreApp.

CoreApp app-routing.module.ts

BTW in Angular 8 Ivy will present new alternative lazy-loaded modules definitions with dynamic import. You can read more about it here.

You may have noticed that in route loadChildren param I have not used app.module.ts (of a respective application), but instead used: app.module-export.ts.

I did this because an application is started separately — we need all the possible modules for its independent work. But for using an application as a module for CoreApp we have to change its main module.

So each sub-application (app1 and app2) has:

  • app.module.ts — for independent development
  • app.module-export.ts — to be used when included in Core-App
App1 app.module.ts
App1 app.module-export.ts

Actually, these two files can be easily merged into one (todo for the future), the used modules can be attached and detached just by using some CoreApp environment.ts variable.

OK, let's try to build it!

Actually, these two files can be easily merged into one (todo for the future), the used modules can be attached and detached just by using some CoreApp environment.ts variable.
OK, let's try to build it!
Compilation failure makes troll sad…

And we got an error:

Error: 
/<some_path>/Core-App/projects/app1/src/app/app.module-export.ts is missing from the TypeScript compilation. Please make sure it is in your tsconfig via the ‘files’ or ‘include’ property.

And the same error for App2.

By default the App1 and App2 typescript code is not part of the main CoreApp project. So we have to configure it in a src/tsconfig.json file.

Configuring typescript in CoreAppsrc/tsconfig.app.json

src/tsconfig.app.json

I’ve just added App1 and App2 typescript to include param array. And also excluded files that are not needed in the main application (like main.ts, test.ts and app.module.ts of App1 and App2 — you remember, we use app.module-export.ts instead).
Running it all separately and together.
Ok, let's run App1 and App2 in standalone mode, and then CoreApp to check how they work together.

After both applications are loaded, the central Store will have this structure:

{
  app1ShowHide: boolean;
  app2ShowHide: boolean;
}

Time to conclude what benefits our solution has!

Benefits of our solution

  • Separation of concerns for feature apps.
  • The main benefit of a monorepo — everything is on the same page and uses the last code version (if you change something in one application — you should update all the other code that uses it).
  • Each application uses NgRx Store feature modules, that are integrated to CoreApp Store as well.
  • You can re-use App1 or App2 in other apps(if needed). And you can even deploy and deliver it independently but some drawbacks are possible (see below).

Drawbacks and obstacles

  • app.module.ts and app.module-export.ts should be kept synchronized.
  • app.module-export.ts of App1 or App2 can introduce something that CoreApp doesn’t provide — then standalone sub-app will work, but the actual app can be broken (smoke test is needed). Solid build all the apps withing CoreApp build is preferable since it will remove the possible re-integrating overhead.
  • Independent sub-application deployment is not implemented in this solution and can be a bit tricky. But if you really want to try — an interesting and quite solid solution can be found here.

To wrap up

Here we go! Mono repo applications App1 and App2 can be lazy-loaded as parts of CoreApp!

All the code I used for this article is located here.

Plz, share your own experience with mono repo and micro-frontend solutions in the comments!

Many thanks to Alex Okrushko, Michael Karén, Wes Grimes, Tim Deschryver for reviewing and providing valuable comments to the article.

I am Angular and RxJS mentor on codementor.io. Have an Angular or RxJS issue or need mentorship? Let me know.

Also, starting from section 4 of my RxJS video course advances staff is reviewed — so if you familiar with RxJS already — you can find something useful for you as well: higher-order observables, anti-patterns, schedulers, unit testing, etc! Give it a try!