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
ordersto import fromshared-uibut not the other way around - we might want to allow
ordersto import fromproductsbut not the other way around - we might want to allow all libraries to import the
shared-uicomponents, 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.
{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.
{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.
{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:featureshould be able to import fromtype:featureandtype:uitype:uishould only be able to import fromtype:uiscope:ordersshould be able to import fromscope:orders,scope:sharedandscope:productsscope:productsshould be able to import fromscope:productsandscope: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:
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:
If you lint your workspace you’ll get an error now:
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:lintIf you have the ESLint plugin installed in your IDE you should also immediately see an error.
Learn more about how to enforce module boundaries.
- Stubbing git
- Installing dependencies