Skip to content

Fix: Nx Not Working — project.json Targets, Affected Commands, Caching, and Generators

FixDevs ·

Quick Answer

How to fix Nx errors — nx.json plugin config, project.json target inputs/outputs, nx affected base branch, cache misses, generator schema, custom executors, and nx migrate failures.

The Error

You run nx build my-app and the project isn’t found:

Cannot find project 'my-app'

Or nx affected rebuilds everything even though you only changed one file:

$ nx affected -t build
# Builds all 50 projects instead of just the affected one.

Or caching reports a hit but the output is wrong:

> nx run my-lib:build
✓ existing outputs match the cache, left as is
# But dist/ has outdated files.

Or nx migrate fails halfway through an upgrade:

Error: An error occurred while migrating. 
Run nx migrate --restore to revert.

Why This Happens

Nx is a monorepo build system that scales to thousands of projects. Its complexity comes from:

  • Two config layers. nx.json is workspace-wide (plugins, target defaults, caching). project.json (per project) defines targets and dependencies.
  • Graph-based affected detection. nx affected computes which projects changed since a base ref and only runs targets for them — but the graph must reflect your dependencies. Missing implicitDependencies or wrong inputs produces false positives or misses.
  • Caching by inputs. Each target hashes its inputs (source files, env vars, command). Identical hash → cache hit. Wrong inputs (missing files, including unrelated ones) breaks caching.
  • Plugin model. Plugins (e.g. @nx/next, @nx/react, @nx/eslint) define generators and executors. Plugin version mismatch with Nx core breaks generators.

Fix 1: Set Up nx.json

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
      "!{projectRoot}/tsconfig.spec.json",
      "!{projectRoot}/.eslintrc.json"
    ],
    "sharedGlobals": [
      "{workspaceRoot}/tsconfig.base.json",
      "{workspaceRoot}/.eslintrc.json"
    ]
  },
  "targetDefaults": {
    "build": {
      "cache": true,
      "inputs": ["production", "^production"],
      "dependsOn": ["^build"]
    },
    "test": {
      "cache": true,
      "inputs": ["default", "^production"],
      "dependsOn": ["^build"]
    },
    "lint": {
      "cache": true,
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"]
    }
  },
  "defaultBase": "main"
}

Three key parts:

  • namedInputs — reusable input sets. production excludes test files.
  • targetDefaults — defaults for targets across all projects. dependsOn: ["^build"] means “before building me, build all my dependencies.”
  • defaultBase — what nx affected compares against by default.

The ^ prefix means “this target on all dependencies.” inputs: ["^production"] means “consider production files in dependencies.”

Pro Tip: Run nx graph to see the inferred dependency graph. If a project that shouldn’t depend on another is connected, your tsconfig.base.json paths or package.json deps imply it. Fix the import or mark implicitDependencies accurately.

Fix 2: Define project.json Targets

Each project (under apps/ or libs/ typically) has a project.json:

{
  "name": "my-app",
  "sourceRoot": "apps/my-app/src",
  "projectType": "application",
  "tags": ["scope:web", "type:app"],
  "targets": {
    "build": {
      "executor": "@nx/webpack:webpack",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/my-app",
        "main": "apps/my-app/src/main.ts",
        "tsConfig": "apps/my-app/tsconfig.app.json",
        "webpackConfig": "apps/my-app/webpack.config.js"
      },
      "configurations": {
        "production": {
          "optimization": true,
          "sourceMap": false
        }
      }
    },
    "serve": {
      "executor": "@nx/webpack:dev-server",
      "options": {
        "buildTarget": "my-app:build"
      }
    }
  }
}

Targets like build, serve, test, lint are callable: nx run my-app:build or nx build my-app.

outputs declare what the target produces — Nx caches these. If outputs is missing or wrong, the cache stores nothing useful and rebuilds always.

For inferred projects (Nx 17+), you may not need project.json at all — Nx auto-detects based on plugins:

// nx.json
{
  "plugins": [
    {
      "plugin": "@nx/next/plugin",
      "options": { "startTargetName": "start", "buildTargetName": "build" }
    }
  ]
}

Run nx show project my-app to see inferred targets.

Common Mistake: Editing inferred targets via project.json. Inferred targets come from the plugin; project.json overrides only specific fields. Use nx show project to confirm what’s effective.

Fix 3: Fix nx affected

nx affected -t build runs build only on projects affected by changes since the base ref:

nx affected -t build --base=main --head=HEAD

# Or use the configured default base:
nx affected -t build

For CI:

# GitHub Actions:
- run: pnpm exec nx affected -t lint build test --parallel=3

Affected detection requires:

  • defaultBase: "main" in nx.json (or --base arg).
  • Git historynx affected walks the git log. Shallow clones miss commits.
  • Correct dependency graph — if A imports from B, changes to B should mark A as affected.

If nx affected runs nothing despite changes:

  • Run nx print-affected --base=main --head=HEAD to see which projects Nx thinks are affected.
  • Check nx graph — visualize the dependency graph.
  • Verify your branch isn’t ahead of main in a weird way: git rev-list --left-right main...HEAD.

If nx affected runs everything:

  • Likely a shared file in namedInputs.sharedGlobals (like tsconfig.base.json) changed.
  • Or a global config (.eslintrc.json at workspace root) changed.

To debug specific affected logic:

nx affected:graph
# Opens a browser visualization of affected projects.

Pro Tip: In CI, fetch the full git history. Shallow clones (fetch-depth: 1 in actions/checkout) break nx affected. Use fetch-depth: 0 for full history.

Fix 4: Cache Configuration

Local cache lives in .nx/cache/. Hashes consider:

  • File contents matching inputs.
  • Target command + options.
  • Environment variables (if declared).
  • Hash of package.json + lockfile.

To debug cache misses:

NX_VERBOSE_LOGGING=true nx build my-app
# Verbose output shows the hash computation.

Look for:

  • Cache miss because of… — Nx tells you which input changed.
  • Cache key: abc123… — record the hash. Re-run and compare; if hash changes between identical inputs, an input isn’t deterministic (timestamps, random IDs).

For env vars affecting the build:

{
  "namedInputs": {
    "production": [
      "default",
      { "env": "NODE_ENV" },
      { "env": "API_URL" }
    ]
  }
}

{ "env": "NODE_ENV" } makes that env var part of the cache key.

To clear cache:

nx reset
# Clears local cache. For Nx Cloud, this doesn't affect remote cache.

For Nx Cloud (remote cache shared across team and CI):

npx nx connect
# Sets up Nx Cloud for the workspace.

This stores cache artifacts in Nx Cloud — CI runs can reuse cache from a teammate’s local build, and vice versa.

Common Mistake: Including too much in inputs. inputs: ["default"] includes test files and configs. Targets that don’t actually depend on those over-invalidate. Use named inputs (production) to scope.

Fix 5: Implicit Dependencies

When project A depends on B but no import shows it (e.g. B is a CLI tool A invokes at build time), declare it:

// apps/my-app/project.json
{
  "name": "my-app",
  "implicitDependencies": ["cli-tool", "shared-config"]
}

implicitDependencies tell Nx “rebuild me if these change,” even without imports.

For library tags + module boundaries:

// nx.json
{
  "tags": ["scope:web", "scope:admin", "type:app", "type:lib"]
}
// .eslintrc.json
{
  "rules": {
    "@nx/enforce-module-boundaries": ["error", {
      "depConstraints": [
        { "sourceTag": "scope:web", "onlyDependOnLibsWithTags": ["scope:web", "scope:shared"] },
        { "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:lib"] }
      ]
    }]
  }
}

This prevents accidental cross-scope imports (e.g. web app importing from admin libs).

Fix 6: Generators

Plugins ship code generators:

nx g @nx/react:lib my-lib --directory=libs/shared/ui
nx g @nx/next:app my-next-app
nx g @nx/node:lib my-node-lib

To customize generation, inspect the generated config and tweak.

For custom generators:

nx g @nx/plugin:plugin my-plugin
nx g @nx/plugin:generator my-generator --project=my-plugin

Define your own scaffolding:

// libs/my-plugin/src/generators/my-generator/generator.ts
import { Tree, formatFiles, generateFiles } from "@nx/devkit";
import * as path from "path";

export default async function (tree: Tree, schema: { name: string }) {
  generateFiles(tree, path.join(__dirname, "files"), `libs/${schema.name}`, {
    name: schema.name,
  });
  await formatFiles(tree);
}

Run:

nx g @my-org/my-plugin:my-generator --name=foo

Common Mistake: Editing files Nx generators created and then re-running the generator. It usually merges (or asks); but conflicting edits get lost. Generate once, then commit; don’t re-run on existing projects.

Fix 7: Migrations

nx migrate automatically upgrades Nx and its plugins:

nx migrate latest        # Pick the latest Nx version
nx migrate @nx/react@21  # Or pin to a specific version

This generates migrations.json listing required migrations. Apply:

nx migrate --run-migrations

Each plugin’s migrations may rewrite configs, update generators, refactor imports.

If a migration fails:

nx migrate --restore
# Reverts the package.json and removes migrations.json.

Then upgrade one plugin at a time:

nx migrate @nx/react@latest
nx migrate --run-migrations
# Verify everything works, then:
nx migrate @nx/next@latest
nx migrate --run-migrations

For major Nx upgrades (e.g. 16 → 17, 17 → 18), read the Nx blog’s migration guide first — some breaking changes need manual intervention.

Pro Tip: Always commit before nx migrate --run-migrations. The diff is large; you want a clean baseline to compare.

Fix 8: Executors and Custom Build Logic

For tasks Nx plugins don’t cover, write a custom executor:

// libs/my-plugin/src/executors/my-executor/executor.ts
import { ExecutorContext } from "@nx/devkit";
import { execSync } from "child_process";

export default async function (
  options: { command: string },
  context: ExecutorContext,
): Promise<{ success: boolean }> {
  try {
    execSync(options.command, {
      cwd: context.root,
      stdio: "inherit",
    });
    return { success: true };
  } catch {
    return { success: false };
  }
}

In executor.json:

{
  "executors": {
    "my-executor": {
      "implementation": "./src/executors/my-executor/executor",
      "schema": "./src/executors/my-executor/schema.json"
    }
  }
}

Use in a project:

{
  "targets": {
    "custom-task": {
      "executor": "@my-org/my-plugin:my-executor",
      "options": {
        "command": "echo hello"
      }
    }
  }
}

For simpler cases, use nx:run-commands (built-in):

{
  "targets": {
    "deploy": {
      "executor": "nx:run-commands",
      "options": {
        "command": "fly deploy",
        "cwd": "{projectRoot}"
      }
    }
  }
}

No custom code; just a shell command run via Nx.

Still Not Working?

A few less-obvious failures:

  • nx affected ignores changes. Check --base argument vs actual base branch. CI may be in detached HEAD state.
  • Caching breaks across machines. Differing Node/pnpm versions affect hash. Pin versions via engines and packageManager in root package.json.
  • Generator fails with Could not find Nx workspace. Run from the workspace root, not from inside a project.
  • Targets disappear after plugin upgrade. Plugin renamed or removed targets. Run nx show project X to see current targets; check the plugin’s migration notes.
  • nx serve doesn’t proxy correctly. Plugins may override webpack/Vite proxy config. Check project.json options for proxy settings.
  • Cannot find module '@nx/...'. A plugin’s transitive dep is missing. pnpm install (or npm install) — Nx peer deps need explicit install in some package managers.
  • Workspace structure tools (apps vs libs). Convention: apps/ for deployable apps; libs/ for shared code. Some plugins assume this layout.
  • Verbose graph rebuilds. Nx caches the project graph in .nx/cache/project-graph. Stale cache: nx reset to clear.

For related monorepo and build tool issues, see Turborepo not working, pnpm workspace not working, Lefthook not working, and TypeScript path alias not working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles