Imposing Constraints on the Dependency Graph

If you partition your code into well-defined cohesive units, even a small organization will end up with a dozen apps and dozens or hundreds of libs. If all of them can depend on each other freely, the chaos will ensue, and the workspace will become unmanageable.

To help with that Nx uses code analyses to make sure projects can only depend on each other's well-defined public API. It also allows you to declaratively impose constraints on how projects can depend on each other.

Tags

Nx comes with a generic mechanism for expressing constraints: tags.

First, use nx.json to annotate your projects with tags. In this example, we will use three tags: scope:client. scope:admin, scope:shared.

1{
2  "npmScope": "myorg",
3  "implicitDependencies": {
4    "package.json": "*",
5    "tsconfig.json": "*",
6    "nx.json": "*"
7  },
8  "projects": {
9    "client": {
10      "tags": ["scope:client"],
11      "implicitDependencies": []
12    },
13    "client-e2e": {
14      "tags": ["scope:client"],
15      "implicitDependencies": ["client"]
16    },
17    "admin": {
18      "tags": ["scope:admin"],
19      "implicitDependencies": []
20    },
21    "admin-e2e": {
22      "tags": ["scope:admin"],
23      "implicitDependencies": ["admin"]
24    },
25    "client-feature-main": {
26      "tags": ["scope:client"],
27      "implicitDependencies": []
28    },
29    "admin-feature-permissions": {
30      "tags": ["scope:admin"],
31      "implicitDependencies": []
32    },
33    "components-shared": {
34      "tags": ["scope:shared"],
35      "implicitDependencies": []
36    }
37  }
38}

Next open the top-level .eslintrc.json or tslint.json to add the constraints.

1{
2  "nx-enforce-module-boundaries": [
3    true,
4    {
5      "allow": [],
6      "depConstraints": [
7        {
8          "sourceTag": "scope:shared",
9          "onlyDependOnLibsWithTags": ["scope:shared"]
10        },
11        {
12          "sourceTag": "scope:admin",
13          "onlyDependOnLibsWithTags": ["scope:shared", "scope:admin"]
14        },
15        {
16          "sourceTag": "scope:client",
17          "onlyDependOnLibsWithTags": ["scope:shared", "scope:client"]
18        }
19      ]
20    }
21  ]
22}

With these constraints in place, scope:client projects can only depend on other scope:client projects or on scope:shared projects. And scope:admin projects can only depend on other scope:admin projects or on scope:shared projects. So scope:client and scope:admin cannot depend on each other.

Projects without any tags cannot depend on any other projects. If you add the following, projects without any tags will be able to depend on any other project.

1{
2  "sourceTag": "*",
3  "onlyDependOnLibsWithTags": ["*"]
4}

If you try to violate the constrains, you will get an error:

A project tagged with "scope:admin" can only depend on projects tagged with "scoped:shared" or "scope:admin".

Exceptions

The "allow": [] are the list of imports that won't fail linting.

  • "allow": ['@myorg/mylib/testing'] allows importing '@myorg/mylib/testing'.
  • "allow": ['@myorg/mylib/*'] allows importing '@myorg/mylib/a' but not '@myorg/mylib/a/b'.
  • "allow": ['@myorg/mylib/**'] allows importing '@myorg/mylib/a' and '@myorg/mylib/a/b'.
  • "allow": ['@myorg/**/testing'] allows importing '@myorg/mylib/testing' and '@myorg/nested/lib/testing'.

Multiple Dimensions

The example above shows using a single dimension: scope. It's the most commonly used one. But you can find other dimensions useful. You can define which projects contain components, state management code, and features, so you, for instance, can disallow projects containing dumb UI components to depend on state management code. You can define which projects are experimental and which are stable, so stable applications cannot depend on experimental projects etc. You can define which projects have server-side code and which have client-side code to make sure your node app doesn't bundle in your frontend framework.