NX : From chaos to consistency - Enforcing Boundaries


Quick personal talk
This feature of NX completely changed my way of developing products, because at the end it’s “just” a nice tool but in fact it’s way more than that. You could build your own framework to do the same thing : enforcing a nice, developer-friendly architecture. But it’ll cost a lot of effort.
And that’s why NX is really nice, it gives you this kind of tool directly. So you don’t spend hours and hours creating your own tool. If I can remember correctly it was one of the goals of the NX team, simplify life of smaller developer teams.
Let me quickly explain how it changed my developer’s life, I will dedicate a full article about other feature that changed my life too, now when I create a product I think deeper about the architecture, I try to split things into libraries, scopes, boundaries… to make them re-usable and easy to maintain. And NX helps me a lot with that.
So let's dive into how these boundary enforcement features work and how you can implement them in your own monorepo.
What is really important
In the last article we talked about how to define libraries types, today we will talk about how we can enforce these rules through linting, dependency constraints, and automated checks so everyone on the monorepo can understand the architecture and be consistent. At the end we will see that you can extend this boundary concept through many things like scope, domains, and team ownership…
So NX give you a new rule on ESLint to add your libraries types, you can find this in eslint config file ( eslint.config.mjs in newer version of NX or .eslintrc.json in older version) at the root of your NX project.
Here is an example on a monorepo with React
import nx from '@nx/eslint-plugin';
export default [
...,
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$'],
depConstraints: [/* Your rules here */]
}
],
},
},
...,
];
Why you need to add a tag on your libraries
NX needs to know which library and of what type in order to apply rules, so with each generation or creation of a library, you'll need to take care to specify its tag.
If you want to create a utility-type library for example, you’ll need to add this in project.json
of your library
"tags": ["type:util"],
or in package.json
of your library
"nx": {
"tags": [
"type:util"
]
}
Of course you can have multiple tags like this
"tags": ["scope:fithelper-front", "type:util"],
// or
"tags": ["scope:fithelper-front", "type:data-access"],
// or
"tags": ["scope:fithelper-front", "type:app"],
// or
"tags": ["scope:shared-components", "type:ui"],
// ...
More here : https://nx.dev/features/enforce-module-boundaries#tags
A slightly more concrete example
Now that we see how to create libraries with tags let’s bring back my previous example (https://gillesferrand.com/nx-library-types)
We want to create a new rule to be sure everything is secured by linting
Let’s start with util rules
import nx from '@nx/eslint-plugin';
export default [
...,
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$'],
depConstraints: [
{
sourceTag: 'type:util',
onlyDependOnLibsWithTags: ['type:util'],
},
]
}
],
},
},
...,
];
With two lines of configuration I warn NX that every libraries tagged util can only import other libraries tagged util. This constraint exists because utility libraries are specifically designed for models, types, and static functions - they should never contain components, features, or services. If we allow util libraries to import from feature libraries, data-access libraries, or ui libraries, we break the foundational nature of these utilities.
This strict rule ensures that util libraries remain purely functional and can serve as the bedrock of your architecture. Components belong in UI libraries (tagged as 'ui'), which can depend on util libraries to access shared models and helper functions, but not the other way around. This one-way dependency flow - where UI components can use utilities, but utilities cannot use components - prevents circular dependencies and keeps your architecture clean and predictable.
Here’s a more complex example
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": [
"type:shell",
"type:feature",
]
},
{
"sourceTag": "scope:fithelper-front",
"onlyDependOnLibsWithTags": [
"scope:fithelper-front",
"scope:shared"
]
},
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": [
"type:feature",
"type:ui",
"type:util",
"type:data-access",
"type:facade"
]
},
{
"sourceTag": "type:data-access",
"onlyDependOnLibsWithTags": ["type:data-access", "type:util"]
},
{
"sourceTag": "type:state",
"onlyDependOnLibsWithTags": ["type:state", "type:util"]
},
{
"sourceTag": "type:facade",
"onlyDependOnLibsWithTags": ["type:state", "type:util"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui", "type:util"]
},
{
"sourceTag": "type:util",
"onlyDependOnLibsWithTags": ["type:util"]
},
{
"sourceTag": "type:shell",
"onlyDependOnLibsWithTags": [
"type:shell",
"type:feature",
"type:data-access",
"type:state",
"type:facade",
"type:ui",
"type:util"
]
}
]
As you can see, you can create whatever boundaries you need : scopes, domains, team ownership, or any custom rules that fit your architecture. NX provides the foundational tools, and you define the constraints that make sense for your project. The more thoughtfully you design these boundaries, the more NX can help you maintain architectural consistency and prevent violations.
The beauty of this approach is its flexibility, but also its potential pitfall. While you can create any boundaries you need resist the urge to over-engineer from the start. Creating 200 scopes and 200 library types will overwhelm your team rather than help them. Start simple with 3-4 clear library types and basic scopes, then add constraints only when you encounter real problems. NX scales with your needs, so let your architecture evolve naturally.
https://nx.dev/concepts/decisions/project-dependency-rules#other-types
Ok.. but how NX enforce it ?
With ESlint !
If I try to import UI library in Util my IDE will warn me that’s is not authorized
And lint job will do it too
nx run shared-local-storage-util:lint
Other ressource
https://www.youtube.com/watch?v=q0en5vlOsWY
https://nx.dev/concepts/decisions/project-dependency-rules#other-types
https://nx.dev/features/enforce-module-boundaries#enforce-module-boundaries
Subscribe to my newsletter
Read articles from Gilles Ferrand directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Gilles Ferrand
Gilles Ferrand
Full stack engineer but passionnated by front-end Angular Expert / NX / JavaScript / Node / Redux State management / Rxjs