How to programmatically enforce boundaries between applications and libraries in an Nx monorepo
In “How to organize and name applications and libraries in an Nx monorepo for immediate team-wide benefits”, we learned that organizing libraries by scope and type offers many immediate team-wide benefits so that all team members can:
- easily find the libraries they need
- immediately identify what type of code a library contains
- immediately know whether or not a specific library can be consumed
Having such clear and strategic guidelines in place is awesome.
However, we, developers, are only human and can easily make mistakes.
In this article, we have a look at how we can use and configure Nx’s awesome code analysis tool to make sure that all applications and libraries in our monorepo truly honor our guidelines.
By the end of this article, you will understand:
- how a well-configured code analysis tool can help us detect human mistakes before they are deployed in production
- how to configure the Nx code analysis tool
- how to run the Nx code analysis tool
So let’s get started!
Library scopes and types
Nx’s code analysis tool makes heavy use of library scope and types.
So before we have a look at Nx’s code analysis tool, let’s refresh our minds and have a super quick look at what we learned about library scopes and types in the previous article.
We learned how to organize libraries strategically by scope and type:
where a library scope (scope-1
, scope-2
) is:
- a logical grouping to organize libraries semantically
- represented by a directory
The goal of a scope is to provide insights in what part of the logic a library deals with, so that:
- all team members can immediately see from the scope whether or not they can consume a library in their application or library
- the maintainers of the library know exactly which applications and libraries can depend on their library and have full awareness of the impact of their changes when they update a library
To name a scope, we use the following rules:
- If a library is built specifically for use in one application, its scope is the application name e.g. “windows-desktop”, “windows-mobile”.
- If a library is built specifically for use in all applications and libraries, its scope is “shared”.
- If a library is built for use in a subset of applications and libraries, its scope is a grouping name e.g. if a library is built for “windows-desktop” and “windows-mobile” but not for other scopes such as “ios”, then we could define a scope with a grouping name “windows”.
In contrast, a library type (type-1
, type-2
) is:
- a logical way to provide a library name with extra semantic value
- represented by a library name prefix
The Enterprise Angular Monorepo Patterns book defines and recommends the following types:
data-access
: contains services and utilities for interacting with back-end systems and all code related to state managementfeature
: contain smart UI (with injected services) for specific business use-cases or pages in an applicationui
: contains only presentational components (that rely only on inputs and outputs)util
: contains common utilities and services used by other libraries and applications
In the previous article, we started with the following applications and libraries:
and we ended with the following structure of libraries organized by scope and type:
This strategically organized structure provides us with immediate team-wide benefits.
So what is the problem?
In an ideal world, our guidelines would be known and applied by all developers in the team and no violations would be made against them.
In reality, however, we have no guarantees that all team members understand and apply our guidelines correctly.
Even if we clearly communicate the guidelines within the team, we have no technical safety-net in place that prevents team members from making any unintended violations or mistakes.
For example, if we consume an icon from windows/ui-icons
inside web-app
, things may technically work and our code may still compile, even if we break the boundary defined by the library scope.
However, when the maintainers of windows/ui-icons
make a change, they will safely assume that web-app
is not consuming the library. As a result, changes may be introduced that break web-app
.
So how do we deal with these situations? How can we be sure that all team members follow the guidelines consistently and stick to the boundaries?
Luckily, Nx has us covered!
Programmatic code analysis
Nx comes with an awesome code analysis tool that allows us to verify programmatically whether all applications and libraries only consume libraries that they have access to.
The tool comes in the form of a TSLint rule that is defined in tslint.json
at the root of the monorepo.
The rule has the following format:
"nx-enforce-module-boundaries": [enable, options]
where:
enable
: boolean, enable or disable the rule
and options
is an object of the following type:
{
allow: string[];
depConstraints: [
{
sourceTag: string;
onlyDependOnLibsWithTags: string[];
}
]
}
where:
allow
: array, whitelist of libraries that can bypass the constraints defined indepConstraints
depConstraints
: array, dependency constraints we wish to apply to our libraries
By default, Nx comes with the following configuration:
which has the following effect:
true
: enable the ruleallow
: empty whitelist, all libraries have to pass the checks indepConstraints
, no exceptions alloweddepConstraints
: make sure all libraries (sourceTag: "*"
) can only depend on all other libraries (onlyDependOnLibsWithTags: ["*"]
)
The default configuration thus allows all libraries to depend on all other libraries.
Now that we know where Nx stores its TSLint rule configuration, let’s have a look at how can we update this configuration to enforce the guidelines from our example.
Tags
If we look carefully at the depConstraints
key of the options object, we can see that Nx refers to tags:
So what are these tags that Nx is referring to?
Tags are metadata that we can attach to applications and libraries via the nx.json
file.
The nx.json
file is a JSON file with a key projects
that contains an entry for each of the projects defined in our monorepo.
A project
can be an application or a library and each project has a key called tags
where we can associate one or more tags with a project.
In our example, we have a project for each of our applications and libraries:
This means we can use tags to associate metadata such as scope and type information with projects.
Because tags are primitive strings, the Enterprise Angular Monorepo Patterns book recommends a clever and concise way to use colons in tags to associate scope and type information with projects:
When we apply that strategy to our example, we get:
There are 2 ways to associate tags with an application or library:
- By editing
nx.json
and adding the tags manually to thetags
property of your application or library. - By specifying
--tags
when generating a new application or library:ng generate lib windows-ui-icons --tags=scope:windows,type:ui
which automatically adds the tags tonx.json
for us.
Make sure not to use spaces in tags so that your command does not break.
In ng generate lib windows-ui-icons --tags=scope: windows
, the space between scope
and windows
would break the command.
Allright! We covered a lot already:
- we learned that Nx provides us with a tslint rule
nx-enforce-module-boundaries
that allows us to define boundaries using tags - we learned that we can associate tags with an application or library via
nx.json
Let’s apply this knowledge to our example and use tags to enforce constraints between our applications and libraries.
Configuring constraints
In the previous section, we already added tags to our applications and libraries to associate each project with a scope and type:
We can now use these tags to configure the boundaries between our applications and libraries via the nx-enforce-module-boundaries
rule in tslint.json
.
In Enterprise Angular Monorepo Patterns, the Nrwl team recommends the following constraints:
- App-specific libraries cannot depend on libraries from other applications.
For example:scope:windows-desktop
can only depend onscope:windows-desktop
andscope:shared
. - Shared libraries cannot depend on app-specific libraries.
For example:scope:shared
cannot depend onscope:windows-desktop
- Ui libraries cannot depend on feature or data-access libraries.
- Util libraries can only depend on other util libraries.
- Data-access libraries cannot depend on feature or ui libraries.
For the sake of demonstration, let’s assume we want to apply these recommended constraints to our example applications and libraries.
Let’s start by defining boundaries between our different scopes:
We can read the scope constraints as follows:
- applications and libraries in scope
electron
can only consume libraries from scopeshared
andelectron
- applications and libraries in scope
shared
can only consume libraries from scopeshared
- applications and libraries in scope
web
can only consume libraries from scopeshared
andweb
- applications and libraries in scope
windows
can only consume libraries from scopeshared
andwindows
- applications and libraries in scope
windows-desktop
can only consume libraries from scopeshared
,windows
andwindows-desktop
- applications and libraries in scope
windows-mobile
can only consume libraries from scopeshared
,windows
andwindows-mobile
Next, let’s add additional boundaries between types:
We can read the type constraints as follows:
- libraries of type
data-access
can only consume libraries of typedata-access
andutil
- libraries of type
ui
can only consume libraries of typeui
andutil
- libraries of type
util
can only consume libraries of typeutil
When we combine our constraints by scope and type, the nx-enforce-module-boundaries
rule in tslint.json
looks like this:
Awesome! Now that we have our configuration in place, it’s time to try it out!
Running
To check all applications and libraries, run:
$ ng lint
To check only the applications and libraries that are affected by your current changes, run:
$ npm run affected:lint
As soon as a boundary is violated against, the lint rule will safely report it.
For example, if a file in the web-app
scope imports a resource from the windows
scope:
then running $ npm run affected:lint
immediately detects and reports the issue:
TSLint: A project tagged with “scope:web-app” can only depend on libs tagged with “scope:web-app”, “scope:shared”(nx-enforce-module-boundaries)
If you have access to a CI tool, you can automate this task and never allow boundary violations to be merged in your codebase again. Hurray!
In addition, modern code editors and IDEs such as VS Code and WebStorm are equipped to understand TSLint rules and can report violations right inside your editor so that you get live feedback when a violation is made:
How sweet is that!
Summary
When developing multiple applications and libraries in a monorepo, it can be surprisingly difficult to name libraries in such a way that all team members can easily find them and know immediately whether or not the library can be consumed.
Organizing libraries by scope and type is a mechanism that helps us conquer that challenge and offers many immediate team-wide benefits.
As a safety-net against unintended boundary violations, Nx provides us with an awesome code analysis tool that allows us to verify whether all applications and libraries in our monorepo truly honor our boundaries.
In this article, we learned:
- what tags are and how they can be used to tag applications and libraries by scope and type via
nx.json
- that Nx provides us with a flexible TSLint rule
nx-enforce-module-boundaries
to define boundaries between applications and libraries using tags - how to translate the recommended constraints from Nrwl’s Enterprise Angular Monorepo Patterns into a TSLint rule
- that running
$ ng lint
checks all applications and libraries and that running$ npm run affected:lint
checks only the applications and libraries that are affected by our current changes
Armed with this knowledge, you can now safely protect your monorepo from boundary violations and rest assured that no unintended violations creep into your code.
Because a well-defined organization of your monorepo truly matters!
Have a great one!