Fix: Nx Not Working — project.json Targets, Affected Commands, Caching, and Generators
Part of: JavaScript & TypeScript Errors
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.jsonis workspace-wide (plugins, target defaults, caching).project.json(per project) defines targets and dependencies. - Graph-based affected detection.
nx affectedcomputes which projects changed since a base ref and only runs targets for them — but the graph must reflect your dependencies. MissingimplicitDependenciesor wronginputsproduces 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.
The root cause of most “Nx is rebuilding everything” frustration is that Nx is conservative by design. If it cannot prove that a target’s inputs are unchanged, it rebuilds. That means a poorly scoped inputs array (["default"]) catches every test file and every config tweak; a global file in sharedGlobals triggers a workspace-wide invalidation; and any non-deterministic step in a build (a timestamped banner, a random suffix) breaks cache reuse on every run. Most performance tuning in Nx is really cache-tuning: getting namedInputs.production to exclude the right things and outputs to declare exactly what gets cached.
The second source of pain is the version of Nx you’re on. Inferred targets (the auto-detection that lets you skip project.json for common stacks) only landed in Nx 17, and many tutorials still describe the older world where every project needs explicit targets. If nx show project my-app lists targets that don’t appear in project.json, they’re coming from a plugin. Trying to override them in project.json works for specific fields but silently ignores entire sections you do not realize the plugin owns. When a plugin upgrade renames a target, the symptom is “my build script disappeared” — the fix is reading the plugin’s migration notes, not editing project.json.
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.productionexcludes test files.targetDefaults— defaults for targets across all projects.dependsOn: ["^build"]means “before building me, build all my dependencies.”defaultBase— whatnx affectedcompares 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 buildFor CI:
# GitHub Actions:
- run: pnpm exec nx affected -t lint build test --parallel=3Affected detection requires:
defaultBase: "main"in nx.json (or--basearg).- Git history —
nx affectedwalks 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=HEADto see which projects Nx thinks are affected. - Check
nx graph— visualize the dependency graph. - Verify your branch isn’t ahead of
mainin a weird way:git rev-list --left-right main...HEAD.
If nx affected runs everything:
- Likely a shared file in
namedInputs.sharedGlobals(liketsconfig.base.json) changed. - Or a global config (
.eslintrc.jsonat 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-libTo 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-pluginDefine 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=fooCommon 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 versionThis generates migrations.json listing required migrations. Apply:
nx migrate --run-migrationsEach 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-migrationsFor 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.
Nx vs Turborepo, Lerna, pnpm Workspaces, Rush, and Moon
Picking a monorepo orchestrator is mostly about how much structure you want imposed and how big the repo will get.
Nx is the most opinionated. It owns the project graph, generates code, ships first-party plugins for React/Next/Node/Angular/NestJS, and offers remote caching (Nx Cloud) out of the box. The cost is config surface and a steep ramp. Use Nx when you have 20+ packages, multiple frameworks, and a team that benefits from generators enforcing layout.
Turborepo is the lightweight competitor. It does task orchestration and caching, nothing else. There’s no code generation, no enforced layout, no built-in module boundary linting. The pipeline is defined per package in turbo.json, the cache is content-hashed, and Vercel’s Remote Cache is a one-line opt-in. Choose Turborepo when you want fast, low-config task running and your repo doesn’t need code generators or strict module boundaries.
Lerna is the original JavaScript monorepo tool. It has been folded into Nx (Nx owns the package now) and gained Nx’s task runner under the hood. The legacy lerna publish flow still works for OSS multi-package release automation, but for new projects, jump straight to Nx and use its publish tools.
pnpm workspaces is the minimum viable monorepo. It hoists deps, hard-links node_modules, and runs scripts per package via pnpm -r run build. No graph, no cache, no plugins — but also no learning curve. Combine with pnpm --filter for selective execution. Choose pnpm workspaces alone when you have 2–5 packages, fast builds, and no need for cross-package change detection. Pair it with Turborepo or Nx once builds get slow.
Rush is Microsoft’s monorepo tool. It is verbose, opinionated, and designed for very large enterprise codebases (think hundreds of packages). It uses pnpm under the hood, has strict policies around dependencies, and is excellent at preventing version drift. Use Rush when you’re at Microsoft scale and need policy enforcement; for everyone else, Nx or Turborepo is less ceremony.
Moon is the newer entrant. It is written in Rust, multi-language (handles Node, Python, Go, Rust in one repo), and ships an inferred project graph plus task pipelines. Moon’s edge is polyglot repos — a single tool orchestrating a TS frontend, a Python API, and a Rust worker. Adoption is smaller than Nx or Turborepo, so the ecosystem is thinner.
Quick rule of thumb: Turborepo for “make it fast with minimum config,” Nx for “I want generators and module boundaries,” pnpm workspaces alone for tiny monorepos, Moon for polyglot, Rush for enterprise policy enforcement.
Still Not Working?
A few less-obvious failures:
nx affectedignores changes. Check--baseargument vs actual base branch. CI may be in detached HEAD state.- Caching breaks across machines. Differing Node/pnpm versions affect hash. Pin versions via
enginesandpackageManagerin rootpackage.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 Xto see current targets; check the plugin’s migration notes. nx servedoesn’t proxy correctly. Plugins may override webpack/Vite proxy config. Checkproject.jsonoptionsfor proxy settings.Cannot find module '@nx/...'. A plugin’s transitive dep is missing.pnpm install(ornpm 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 resetto clear. - Nx Cloud cache hits locally but misses in CI. The hash includes the OS architecture by default — a macOS dev cache won’t reuse on Linux CI. Either accept the split or use the
--skip-nx-cacheflag for debugging while you investigate. pnpm installregenerates lockfile and breaks the cache. Lockfile changes invalidate every cached task because the workspace hash includes it. PinpackageManagerinpackage.jsonand disable auto-update behavior in your dev env to keep the lockfile stable.nx releaseskips a project on publish. A project without aversionfield inpackage.jsonor marked"private": trueis excluded by default. Checknx release --dry-runto see which projects participate.
For related monorepo and build tool issues, see Turborepo not working, pnpm workspace not working, Lefthook not working, and TypeScript path alias not working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: pnpm Catalog Protocol Not Working — Cannot Find Catalog, Resolution Errors, and Lockfile Issues
Fix pnpm 9.5+ catalog protocol errors — Cannot find catalog default, ERR_PNPM_CATALOG_ENTRY_INVALID_SPEC, stale lockfile state, and tool incompatibility with catalog: references in monorepos.
Fix: Bun Bundler Not Working — Targets, Format, Externals, Macros, and Code Splitting
How to fix bun build errors — target (browser/bun/node) mismatch, format esm/cjs/iife, externals not respected, Bun macros at compile time, splitting and chunks, plugin API, and Bun.build vs CLI.
Fix: Bun Shell Not Working — $ Template Quoting, Pipes, Exit Codes, and Cross-Platform Scripts
How to fix Bun Shell errors — $ template auto-escape vs raw strings, piping with pipe() vs |, throws on non-zero exit, cwd/env scoping, glob expansion differences, and Windows path handling.
Fix: ESLint Flat Config Not Working — eslint.config.js, ignores, Plugins, and Migration
How to fix ESLint flat config errors — eslint.config.js not found, .eslintrc.json ignored after upgrade, ignores replacing .eslintignore, plugin object form, typescript-eslint integration, monorepo configs, and ESLINT_USE_FLAT_CONFIG.