Claude Code with Nx: Monorepo Orchestration, Project Graph, and Generators
Why Nx monorepos break Claude Code's defaults
Claude Code is a strong individual contributor. It reads files, writes code, runs commands, and iterates on feedback. But it is trained on single-package thinking. It expects one package.json at the root, a src/ folder, and commands like npm test and npm run build.
Nx inverts all of that.
An Nx workspace has dozens or hundreds of projects, each with its own project.json, its own build targets, its own tags, and its own position in a dependency graph that Nx maintains and enforces. The commands that matter are not npm test but nx affected:test, not npm run build:all but nx affected:build. Cross-project imports are governed by lint rules derived from tags in nx.json. Generators scaffold new apps and libraries with conventions baked in.
Without configuration, Claude Code treats your Nx workspace like a big single-package repo. It runs full builds when affected would suffice, imports across project boundaries without checking constraints, writes project.json entries manually instead of using generators, and ignores the dependency graph entirely.
The fix is a CLAUDE.md that teaches Claude the Nx mental model once and enforces it across every session. This guide covers that configuration, the five failure modes to prevent, affected command usage, module boundary enforcement, generator workflow, and Nx Cloud caching habits.
If you are starting from scratch, Claude Code setup guide covers installation. For the broader monorepo context, Claude Code for monorepos is worth reading alongside this guide. For TypeScript conventions that apply across your Nx workspace, Claude Code with TypeScript covers the type-safety layer.
The Nx CLAUDE.md template
Place this at the workspace root. Adjust the workspace layout, stack, and tag taxonomy to match your project. The rest applies to any Nx workspace regardless of framework.
# Nx workspace rules
## Workspace layout
- apps/ : deployable applications (web, api, mobile, workers)
- libs/ : shared libraries (feature libs, data-access libs, UI libs, utility libs)
- tools/ : workspace tooling (generators, executors, scripts)
- nx.json : global Nx configuration, target defaults, tag constraints
- project.json : per-project configuration (targets, tags, sourceRoot)
## Package manager
- pnpm workspaces (pnpm-workspace.yaml at root)
- Node version: 20.x enforced via .nvmrc
- Never use npm or yarn commands in this repo
## Project graph rules
- The project graph is the source of truth for dependency relationships
- Run "nx graph" to visualise the current graph before planning cross-project changes
- Never modify a project's dependencies without checking graph impact
- Circular dependencies are hard errors. If you create one, stop and resolve it.
- Implicit dependencies (from nx.json "implicitDependencies") affect all projects. Edit with care.
## Affected commands (MANDATORY)
- NEVER run nx build/test/lint on all projects at once
- ALWAYS use nx affected:build, nx affected:test, nx affected:lint for CI-equivalent runs
- For local development on a specific project: nx build {project}, nx test {project}
- nx affected uses the base branch (main) as comparison by default
- To specify the base: nx affected:test --base=main --head=HEAD
- Affected is based on the project graph, not just changed files. A lib change
affects all apps that consume it, even if you only changed one line.
## Running targets
- nx {target} {project} , single project
- nx run-many -t {target} --all , all projects (use sparingly, prefer affected)
- nx affected:{target} , affected projects only (default for CI)
- nx {target} {project} --skip-nx-cache , force re-run ignoring cache (debugging only)
- nx {target} {project} --verbose , detailed output for debugging
## Generators, the only way to scaffold
- NEVER create a new lib or app by copying files or writing project.json manually
- ALWAYS use nx generators for new projects and code scaffolding
- New React library: nx g @nx/react:lib {name} --directory=libs/{scope}/{name} --tags="scope:{scope},type:{type}"
- New Next.js app: nx g @nx/next:app {name} --directory=apps/{name} --tags="scope:{scope},type:app"
- New Node library: nx g @nx/node:lib {name} --directory=libs/{scope}/{name} --tags="scope:{scope},type:{type}"
- New component: nx g @nx/react:component {name} --project={project} --export
- Dry run first: append --dry-run to preview what will be generated
- Always pass --tags with scope and type tags matching the taxonomy below
- Never use --interactive (Claude runs non-interactive only)
## Tag taxonomy (MANDATORY, every project must have both tags)
- scope tags: scope:shared, scope:orders, scope:payments, scope:auth, scope:admin, scope:public
- type tags: type:app, type:feature, type:data-access, type:ui, type:utility, type:e2e
## Module boundary constraints (enforced by ESLint @nx/enforce-module-boundaries)
- type:app , can import from any type
- type:feature , can import from data-access, ui, utility; NOT from app or other feature libs
- type:data-access, can import from utility; NOT from feature, ui, or app
- type:ui , can import from utility; NOT from feature, data-access, or app
- type:utility , MUST NOT import from any other lib type (leaf node)
- scope:shared , can be imported by any scope
- Cross-scope imports (e.g. scope:orders importing scope:payments) require explicit review
## Project.json conventions
- sourceRoot must be "libs/{scope}/{name}/src" or "apps/{name}/src"
- targets: build, test, lint must exist on every project
- test executor: @nx/jest:jest (or @nx/vitest:vitest if the project uses Vitest)
- lint executor: @nx/eslint:lint
- build executor: varies by project type, check existing projects, never guess
## Nx Cloud caching
- Remote caching is enabled, do NOT run nx {target} {project} --skip-nx-cache unless
you are actively debugging a caching bug
- If a target result looks stale, first check: nx show project {project} --web
- Cache hits are expected for unchanged inputs, a cache hit is not an error
- To clear local cache only: nx reset
- Never add --skip-nx-cache to shared scripts or CI configurations
## What NOT to do
- Do NOT run "npm run build" or "pnpm build" at the root, use nx targets
- Do NOT add scripts to root package.json for individual project builds
- Do NOT create project.json manually, use nx generators
- Do NOT import from another project's internal files (only from its index.ts barrel)
- Do NOT add a new project without tags, untagged projects break boundary enforcement
- Do NOT modify .eslintrc.base.json module boundary rules without confirming the constraint
is intentional, those rules are the architecture made executable
That block is 48 lines of Nx-specific constraints. Copy it, update the scope tags and stack for your workspace, and Claude follows the Nx mental model for every session.
The critical entries are the affected commands section and the generator section. Together they prevent the two highest-frequency errors: running full workspace builds and creating projects by hand.
Five Claude defaults that break Nx workspaces
These are the failure modes you will hit without a CLAUDE.md. Each one is a reasonable default for a single-package project that becomes a serious problem in an Nx monorepo.
1. Running full builds instead of affected
Without guidance, Claude runs:
nx run-many -t build --all
# or worse
pnpm build
In a 40-project workspace, this is a 20-minute build. The correct command is:
nx affected:build --base=main --head=HEAD
Nx builds only the projects whose inputs have changed, plus all projects that depend on them transitively. A change to libs/shared/utils might affect 12 apps, a change to libs/orders/feat-checkout might affect 2. Nx computes this from the project graph. Running --all ignores the graph entirely and rebuilds everything regardless of what changed.
The CLAUDE.md entry enforces this: "NEVER run nx build/test/lint on all projects at once. ALWAYS use nx affected."
2. Importing across module boundaries
Claude Code will happily write:
// In libs/orders/feat-checkout/src/lib/checkout-form.tsx
import { AuthService } from '@myapp/auth-data-access';
This import violates two constraints: a type:feature lib importing from a different scope, and potentially a circular dependency if auth-data-access has any indirect dependency on orders. The @nx/enforce-module-boundaries ESLint rule catches this, but only if lint runs.
Claude does not check module boundaries before writing imports. It checks whether the TypeScript types resolve, which they might even if the architectural constraint is violated. The CLAUDE.md entry names the constraint table explicitly so Claude reasons about boundary compliance before writing the import, not after.
3. Creating new projects by hand
Without a generator rule, Claude will:
mkdir -p libs/payments/ui-card/src/lib
touch libs/payments/ui-card/src/index.ts
touch libs/payments/ui-card/project.json
And then write a project.json from scratch, usually missing executor options, getting the sourceRoot wrong, and forgetting to add ESLint and test targets. The resulting project is not wired into the Nx project graph correctly.
The generator equivalent:
nx g @nx/react:lib ui-card \
--directory=libs/payments/ui-card \
--tags="scope:payments,type:ui" \
--dry-run
The --dry-run flag previews what will be created without writing anything. Review the output, remove --dry-run, and run again. The generator creates the correct project.json, wires the project into the graph, sets up Jest or Vitest configuration, and adds the ESLint config extending the workspace base. None of that happens when Claude creates files manually.
4. Ignoring Nx Cloud cache hits
Claude sometimes treats a cache hit as a skip that might hide a real failure:
nx test orders-feat-checkout --skip-nx-cache
Cache hits in Nx are deterministic. Nx hashes all inputs: source files, dependencies, environment variables declared in nx.json, and the executor version. If the hash matches a cached run, the output is identical. Adding --skip-nx-cache re-runs the full test suite, consuming CI minutes and local time for no new information.
The CLAUDE.md entry blocks this: "--skip-nx-cache is for debugging caching bugs only, not routine test runs."
5. Reading project configuration from the wrong place
Nx workspaces can use either project.json (explicit) or package.json (implicit, for smaller packages). Some workspaces mix both. Claude will grep for package.json files to understand project structure and sometimes miss or misread projects defined only in project.json.
The correct command to inspect a project:
nx show project {project-name}
This outputs the merged, fully-resolved project configuration including all targets, dependencies, and tags, regardless of whether the project uses project.json or package.json. Add this to your CLAUDE.md under the retrieval map: "To inspect a project's resolved config: nx show project {name}."
Using affected commands correctly
The affected command set is the most important Nx feature for Claude Code workflow. Here is the full set with the flags Claude needs to know:
# Test only affected projects
nx affected:test --base=main --head=HEAD
# Build only affected projects
nx affected:build --base=main --head=HEAD
# Lint only affected projects
nx affected:lint --base=main --head=HEAD
# Graph of affected projects (useful before a big change)
nx affected:graph --base=main --head=HEAD
# Print affected project names (for scripting)
nx print-affected --select=projects --base=main --head=HEAD
# Run multiple targets on affected in sequence
nx affected --target=lint,test,build --base=main --head=HEAD
For local development on a feature branch, --base=main --head=HEAD covers the diff between your branch and main. For CI, Nx typically sets NX_BASE and NX_HEAD environment variables automatically, so nx affected:test without flags works in the pipeline.
The key insight for Claude: affected is not "files that changed." It is "projects whose inputs changed, plus all projects that depend on them." If you change a file in libs/shared/utils, Nx traverses the project graph upward and includes every project that imports from shared/utils, transitively. A one-line change in a widely-used utility lib can make 30 projects affected. That is correct behaviour and tells you about your dependency health.
Running generators
Generators are the sanctioned way to add projects and scaffold code in an Nx workspace. Here is the workflow Claude should follow for common scaffolding tasks:
Adding a new library
# Dry run first to preview
nx g @nx/react:lib feat-user-profile \
--directory=libs/users/feat-user-profile \
--tags="scope:users,type:feature" \
--dry-run
# If the preview looks correct, run without --dry-run
nx g @nx/react:lib feat-user-profile \
--directory=libs/users/feat-user-profile \
--tags="scope:users,type:feature"
After the generator runs:
# Verify the project appears in the graph
nx show project users-feat-user-profile
# Verify boundary rules pass on the new project
nx lint users-feat-user-profile
Adding a component to an existing library
nx g @nx/react:component UserProfileCard \
--project=users-feat-user-profile \
--export
The --export flag adds the component to the library's index.ts barrel file, making it importable by consumers. Without it, the component exists but is not part of the public API.
Adding a new application
nx g @nx/next:app web-portal \
--directory=apps/web-portal \
--tags="scope:public,type:app" \
--dry-run
Always pass --tags when creating any project. Untagged projects cannot be constrained by module boundary rules and silently bypass the architecture enforcement that nx.json encodes.
Nx workspace generators (custom generators)
If your workspace has custom generators in tools/generators/, Claude should use them:
# List available workspace generators
nx list
# Run a custom generator
nx g @myapp/workspace:feature-lib my-feature \
--scope=orders \
--dry-run
Custom generators encode your team's conventions: folder structure, file naming, boilerplate, and tags. They are more reliable than built-in generators for workspace-specific patterns.
Module boundary enforcement in practice
The @nx/enforce-module-boundaries rule in libs/.eslintrc.json (or eslint.config.mjs) is the architectural contract made executable. Here is what a typical constraint block looks like:
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependencyRule": true,
"depConstraints": [
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:data-access", "type:ui", "type:utility"]
},
{
"sourceTag": "type:data-access",
"onlyDependOnLibsWithTags": ["type:utility"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:utility"]
},
{
"sourceTag": "type:utility",
"onlyDependOnLibsWithTags": []
}
]
}
]
}
}
When Claude writes an import that violates these rules, the linter catches it. But the linter only runs when you run lint. The CLAUDE.md tells Claude to reason about the constraint table before writing imports, not to rely on the linter as a safety net after.
Add this to your CLAUDE.md under "When Claude needs to add an import from another lib":
## Import decision process
1. Find the importing project's tags: nx show project {project} | grep tags
2. Find the target library's tags: nx show project {lib} | grep tags
3. Check the depConstraints table above, is this import permitted?
4. If yes: import from the library's index.ts barrel only, never from internal paths
5. If no: either use a different lib, move the shared code to a utility lib, or flag
the architectural question before writing the import
This turns boundary enforcement from a lint-time failure into a design-time check. Claude reasons about the constraint before writing the code, which is the correct order.
Nx Cloud and caching
Nx Cloud provides remote caching: task outputs are cached in the cloud and shared across machines. When a team member runs nx test my-lib and gets a cache hit, they are reusing the cached output from the CI run that computed it.
For Claude Code, the relevant habits are:
# Check whether a task has a recent cache entry before re-running
nx show project {project} --web
# Clear only the local cache (does not affect remote cache)
nx reset
# Run without cache for genuine debugging
nx test {project} --skip-nx-cache --verbose
# Force all outputs to be computed and written to remote cache
nx affected:build --base=main --skip-nx-cache
The --skip-nx-cache flag should appear in Claude sessions only when the user explicitly says "the cache seems wrong" or "force a fresh build." It must not appear in any script, CI step, or routine command that Claude writes. Remote cache hits represent CPU-hours saved; bypassing them for routine runs wastes the team's credits.
Add to your CLAUDE.md:
## Cache rules
- Never add --skip-nx-cache to scripts, CI configs, or package.json tasks
- --skip-nx-cache is only for interactive debugging of cache correctness
- If a CI run fails but local passes, check inputs first: nx print-affected --select=projects
- nx reset clears only the local cache, not the remote cache
Project.json anatomy
Understanding project.json helps Claude navigate existing projects and avoid rewriting configuration it does not understand. Here is a minimal but complete project.json:
{
"name": "orders-feat-checkout",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/orders/feat-checkout/src",
"projectType": "library",
"tags": ["scope:orders", "type:feature"],
"targets": {
"build": {
"executor": "@nx/vite:build",
"options": {
"outputPath": "dist/libs/orders/feat-checkout"
}
},
"test": {
"executor": "@nx/vitest:vitest",
"options": {
"passWithNoTests": true
}
},
"lint": {
"executor": "@nx/eslint:lint",
"options": {
"lintFilePatterns": ["libs/orders/feat-checkout/**/*.{ts,tsx}"]
}
}
}
}
Claude should read the project.json of an existing project before adding a new target or modifying an executor option. The executor version, output path pattern, and options structure vary between project types and Nx plugin versions. Guessing them leads to broken targets that fail silently or produce incorrect output paths.
Add to your CLAUDE.md: "Before modifying any project.json target: read the project.json of a similar existing project and mirror its executor options."
FAQ
Can I use Claude Code without a CLAUDE.md in an Nx workspace?
Technically yes. Practically, Claude will break your module boundaries, run full builds, and create projects manually within the first two sessions. The CLAUDE.md pays for itself in the first hour of use.
Does nx configure-ai-agents replace the CLAUDE.md approach?
nx configure-ai-agents (from the official Nx docs) sets up the Nx MCP server and generates a starter CLAUDE.md with workspace context. It is a good starting point. The template in this guide goes further on failure modes, generator workflow, and cache habits. Use the Nx command to bootstrap, then overlay the patterns here.
How does Claude know which projects are affected?
It does not intrinsically. It needs to run nx print-affected --select=projects to get the list, or check nx affected:graph for a visual. Add a prompt to your CLAUDE.md: "Before any test or build task, run nx print-affected --select=projects to understand scope."
What happens if a generator creates something I do not want?
Generators are reversible during development. Run git checkout . to undo an unwanted generator output. For this reason, always dry-run first and review before committing.
Should every lib have a build target?
Not necessarily. Publishable and buildable libs need a build target. Internal libs that are only imported at type-check time do not. Your nx.json targetDefaults can set this at the workspace level. Check nx.json before adding or removing build targets on individual projects.
Running Nx at scale with Claude Code requires the same discipline as any other tool in your pipeline: explicit rules, enforced conventions, and no assumptions. The CLAUDE.md template above encodes the Nx mental model so Claude applies it consistently. For the broader monorepo workflow, the TypeScript conventions that span your workspace, or the testing strategy that runs across projects, the pattern is the same: write the rules once, enforce them in configuration, and let Claude execute within those constraints.
Claudify ships Nx-ready CLAUDE.md templates, affected-command defaults, and generator workflow patterns tested on real production monorepos. One command: npx create-claudify.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify