Understanding Target Dependencies in NX

Travis Jones
3 min readNov 27, 2021

I’m a huge fan of Nrwl’s NX, and use it to manage several different monorepos. When using NX, I’ve found many instances in which I want some executable action to depend on some executable action. In this article, I’ll outline how to set up target dependencies, and even how to gain some extra control over its execution, based on a recent experience.

Before Diving In

If you’re reading this, you may already know a bit about NX, but just so everything’s clear, I’ll start with explaining a couple terms:

Target — A named executable action that is configured per project. Common targets include lint, build, and serve.

Dependency Graph — One of NX’s most powerful features, it shows which of the projects in your monorepo depend on one another.

Setting Up Target Dependencies

Let’s say that with one of your targets, build for this example, you want another target, pre-build, to automatically execute before it. NX has a convenient way to set up such a dependency. To do this you set up the relationship between these targets in your project’s configuration (either in workspace.json in the monorepo root, or the project.json in your project’s root), like so:

...
"targets": {
"build": {
...,
"dependsOn": [
{
"target": "pre-build",
"projects": "self"
}
]
},
"pre-build": {...}
}

The dependsOn property gets defined on a particular target, and contains a list of targets it depends on running, prior to itself. The actual items in the dependsOn list establish a target that should be executed either in the same project, indicated by self, or the other dependent projects in the dependency graph, using dependencies. Now, if we run nx build {my-app} it will run this same project’s pre-build target first.

Setting a Target Dependency for Every Project

The above solution is nice, but if you need to set the same target dependency for each project in your monorepo, then you probably want a single location you can set it up for all projects. To do that, we go to the nx.json file located in the root of your monorepo. In there, we can place some very similar configuration:

{
...,
"targetDependencies": {
"build": [
{
"target": "pre-build",
"projects": "self"
}
]
},
...
}

First, the targetDependencies property sets up dependencies between targets across the entire workspace. With the above configuration, any project you run the build target on will first try to run pre-build.

Target Dependencies on… Dependencies

Let’s say that, instead of running another target in the same project, we want to run a target on the project’s dependencies. A good example of a use-case for this is running automated tests, using the test target, on only the libraries related to a single application in your monorepo. In this case you can add the following configuration to your nx.json:

{
...,
"targetDependencies": {
"test": [
{
"target": "test",
"projects": "dependencies"
}
]
},
...
}

Or, you could set its analogous settings in a dependsOn inside the project’s configuration (first example). However, there are some key differences between the two…

Target Dependencies, Project vs Workspace

There are some key behavioral differences when you set up the dependency on a project versus workspace, mainly that when a project refers to its “dependencies”, it means only the direct dependencies. Thus, if you want to run a target on every dependency, through to the deepest dependency, then you’ll likely want to set it up in your nx.json. If you do this, it will queue up the targets to execute in the order of most depended on to least.

One additional difference is that if you set up similar target dependencies in your nx.json and a project.json, the one for your project.json will take precedence.

Cautionary Note on Mixing “self” and “dependencies”

Let me start off by saying that having multiple target dependencies, some specifying self, and others dependencies, absolutely works. However, it is not deterministic. That is, you can’t expect a particular order. Let’s say, for example, that you set up a configuration like:

{
...,
"targetDependencies": {
"build": [
{
"target": "build",
"projects": "dependencies"
}
],
"pre-build": [
{
"target": "pre-build",
"projects": "self"
}
]
},
...
}

With this setup, you may expect an execution sequence like

  1. myLib -> pre-build
  2. myLib -> build
  3. myApp -> pre-build
  4. myApp -> build

but may instead get an execution sequence where all of the pre-builds run, followed by the builds. If you need a project to execute its own targets in a particular order, one option is to set up a custom target using the @nrwl/workspace:run-commands executor and list the commands to run, with parallel execution of the commands disabled.

--

--