Imposing Constraints with Module Boundary Rules

Once you modularize your codebase you want to make sure that the libs are not coupled to each other in an uncontrolled way. Here are some examples of how we might want to guard our small demo workspace:

  • we might want to allow orders to import from shared-ui but not the other way around
  • we might want to allow orders to import from products but not the other way around
  • we might want to allow all libraries to import the shared-ui components, but not the other way around

When building these kinds of constraints you usually have two dimensions:

  • type of project: what is the type of your library. Example: “feature” library, “utility” library, “data-access” library, “ui” library
  • scope (domain) of the project: what domain area is covered by the project. Example: “orders”, “products”, “shared” … this really depends on the type of product you’re developing

Nx comes with a generic mechanism that allows you to assign “tags” to projects. “tags” are arbitrary strings you can assign to a project that can be used later when defining boundaries between projects. For example, go to the project.json of your orders library and assign the tags type:feature and scope:orders to it.

/libs/orders/project.json
{
4 collapsed lines
"name": "orders",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/orders/src",
"projectType": "library",
"tags": ["type:feature", "scope:orders"],
13 collapsed lines
"// targets": "to see all targets run: nx show project orders --web",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/orders/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

Then go to the project.json of your products library and assign the tags type:feature and scope:products to it.

/libs/products/project.json
{
4 collapsed lines
"name": "products",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/products/src",
"projectType": "library",
"tags": ["type:feature", "scope:products"],
13 collapsed lines
"// targets": "to see all targets run: nx show project products --web",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/products/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

Finally, go to the project.json of the shared-ui library and assign the tags type:ui and scope:shared to it.

/libs/shared/ui/project.json
{
4 collapsed lines
"name": "ui",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/ui/src",
"projectType": "library",
"tags": ["type:ui", "scope:shared"],
13 collapsed lines
"// targets": "to see all targets run: nx show project ui --web",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/ui/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

Notice how we assign scope:shared to our UI library because it is intended to be used throughout the workspace.

Next, let’s come up with a set of rules based on these tags:

  • type:feature should be able to import from type:feature and type:ui
  • type:ui should only be able to import from type:ui
  • scope:orders should be able to import from scope:orders, scope:shared and scope:products
  • scope:products should be able to import from scope:products and scope:shared

To enforce the rules, Nx ships with a custom ESLint rule. Open the eslint.config.mjs at the root of the workspace and add the following depConstraints in the @nx/enforce-module-boundaries rule configuration:

/eslint.config.mjs
2 collapsed lines
import nx from '@nx/eslint-plugin';
export default [
6 collapsed lines
...nx.configs['flat/base'],
...nx.configs['flat/typescript'],
...nx.configs['flat/javascript'],
{
ignores: ['**/dist'],
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
depConstraints: [
{
sourceTag: "type:feature",
onlyDependOnLibsWithTags: ["type:feature", "type:ui"]
},
{
sourceTag: "type:ui",
onlyDependOnLibsWithTags: ["type:ui"]
},
{
sourceTag: "scope:orders",
onlyDependOnLibsWithTags: [
"scope:orders",
"scope:products",
"scope:shared"
]
},
{
sourceTag: "scope:products",
onlyDependOnLibsWithTags: ["scope:products", "scope:shared"]
},
{
sourceTag: "scope:shared",
onlyDependOnLibsWithTags: ["scope:shared"]
},
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
14 collapsed lines
{
files: [
'**/*.ts',
'**/*.tsx',
'**/*.cts',
'**/*.mts',
'**/*.js',
'**/*.jsx',
'**/*.cjs',
'**/*.mjs',
],
// Override or add rules here
rules: {},
},
];

To test it, go to your /libs/products/src/lib/products/products.component.ts file and import the Orders component from the orders project:

/libs/products/src/lib/products/products.component.ts

If you lint your workspace you’ll get an error now:

Terminal window
nx run-many -t lint
Running target lint for 7 projects
✖ nx run products:lint
Linting "products"...
/home/tutorial/libs/products/src/lib/products/products.component.ts
5:1 error A project tagged with "scope:products" can only depend on libs tagged with "scope:products", "scope:shared" @nx/enforce-module-boundaries
5:10 warning 'OrdersComponent' is defined but never used @typescript-eslint/no-unused-vars
✖ 2 problems (1 error, 1 warning)
Lint warnings found in the listed files.
Lint errors found in the listed files.
✔ nx run orders:lint (996ms)
✔ nx run angular-store:lint (1s)
✔ nx run angular-store-e2e:lint (581ms)
✔ nx run inventory-e2e:lint (588ms)
✔ nx run inventory:lint (836ms)
✔ nx run shared-ui:lint (753ms)
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
NX Ran target lint for 7 projects (2s)
✔ 6/7 succeeded [0 read from cache]
✖ 1/7 targets failed, including the following:
- nx run products:lint

If you have the ESLint plugin installed in your IDE you should also immediately see an error.

Learn more about how to enforce module boundaries.

Powered by WebContainers
Files
Preparing Environment
  • Stubbing git
  • Installing dependencies