Sergio Ausin | ng-conf | Feb 2019
Whenever you start a new project, one of the first decisions we made is the structure of our code base. This is important because we do not want to have spaghetti code, features breaking when code changes... A few years ago, I worked in a project that comprises several apps with shared libraries. At first, everything was fine: apps were very small, few requirements; but every small app converts into a big one. The problem appeared when the first critical bug was reported. The typical steps to solve any bug were:
- Identify the cause
- Patch the affected code
- Build a new version of the library. We used semantic versioning
- Upload it to the private NPM repository
- Update every app using the library
- Verify that everything is working as intended (launch e2e tests, unit tests, etc.)
- Finally, we released a new version of the apps
Obviously, if something went wrong we have to repeat some of these steps. This process may be a pain in the ass when you have to update several applications. On the other hand, you also have to be aware of the applications that are using the affected library. I wasted a lot of time upgrading libraries, so I decided to look for a better way to deal with this problem: the monorepository. The main advantages of this approach are:
- Same library version for every app
- Ease of maintenance: when you update a shared library, you update it for all apps.
- No conflicts between versions
This approach is followed by Google and Facebook, among others. So, if you did not know it, you should give it a try! To help us with this task, the guys from Nrwl have created NX.
NX by Nrwl
NX is an extension for the @angular/cli that implements the monorepo-style development. It provides a set of code generators, libraries, linters…
For example, we can create an application with layered architecture: logic, user interface, etc. The picture below shows an example of layered architecture:
Monorepo folder structure
In the libs folder, there is a “three level” directory tree.
- First level: the name of the apps. Inside this directory, there are the libs that our app will use
- Second level: the use case/page directory
- Third level: the “layer” directory. There are some typical libraries that I use: core for services, user-interface for components and a routing library which is lazy loaded by the app.
This architecture follows these rules:
- A core library must not import a user interface library
- A core library must not import a routes library
- A user interface library must not import a routes library
- A shared library must not import a non-shared library
- A use case library must not import another use case library
- A library must not import an app
To be consistent and make sure that we are following correctly the layered architecture, NX provides us a lint rule: “nx-enforce-module-boundaries”. To make it works, we have to set up some tags in every library. Those tags are set up in the nx.json file:
{
"npmScope": "ngconf",
"implicitDependencies": {
"angular.json": "",
"package.json": "",
"tsconfig.json": "",
"tslint.json": "",
"nx.json": "*"
},
"projects": {
"web-app": {
"tags": []
},
"web-app-e2e": {
"tags": []
},
"web-app-home-core": {
"tags": [
"scope:home",
"type:core"
]
},
"web-app-home-user-interface": {
"tags": [
"scope:home",
"type:user-interface"
]
},
"web-app-home-routes": {
"tags": [
"scope:home",
"type:routes"
]
},
"web-app-workshops-core": {
"tags": [
"scope:workshops",
"type:core"
]
}
}
}
nx.json hosted by GitHub
For this example, I have used the tags:
- Scope: for the use case
- Type: for the layer
Once we have set up correctly our nx.json file, we have to add the “nx-enforce-module-bundaries” rule to our tslint.json file.
{
...
"rules": {
... // other tslint rules
"nx-enforce-module-boundaries": [
true,
{
"allow": [],
"depConstraints": [
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": [
"scope:shared"
]
},
{
"sourceTag": "scope:home",
"onlyDependOnLibsWithTags": [
"scope:home",
"scope:shared"
]
},
{
"sourceTag": "type:core",
"onlyDependOnLibsWithTags": [
"type:core"
]
},
{
"sourceTag": "type: user-interface",
"onlyDependOnLibsWithTags": [
"type:core"
]
}
]
}
]
}
}
tslint.json hosted by GitHub
With this simple configuration we achieve the following goals:
- Home libraries can import home and shared libs
- Core libraries can only import core libraries
- user-interface libraries can only import core libraries
If you try to import a user-interface library from a core lib you will see the following error message:
A project tagged with “type:core” can only depend on libs tagged with “type:core”
To sum up
NX enforces quality and consistency when we develop our apps. It gives us some good practices that we should follow when we are developing our apps!
EnterpriseNG is coming November 4th & 5th, 2021.
Come hear top community speakers, experts, leaders, and the Angular team present for 2 stacked days on everything you need to make the most of Angular in your enterprise applications.
Topics will be focused on the following four areas:
• Monorepos
• Micro frontends
• Performance & Scalability
• Maintainability & Quality
Learn more here >> https://enterprise.ng-conf.org/
Top comments (0)