Nx Monorepo Guide 2026: Scale Your Codebase with Smart Build Caching
A complete guide to Nx monorepo: setup, generators, smart build caching, CI integration, and how it compares to Turborepo. Learn to scale your codebase with Nx.
Monorepos solve a real problem: when you have multiple apps and libraries that need to share code, the alternative is dependency hell. But monorepos introduce their own problems β slow builds, complex CI pipelines, inconsistent tooling. Nx was built to solve exactly those problems.
Nx is a build system and monorepo tool that brings intelligent task scheduling, caching, and generators to any JavaScript/TypeScript codebase. In 2026, itβs used by thousands of teams including Google, Microsoft, and Shopify for codebases with hundreds of projects.
This guide covers everything you need to set up, configure, and scale a monorepo with Nx.
Why Nx?
The Monorepo Problem It Solves
Without Nx, a monorepo build system typically means:
- Running tests for everything on every CI run (even when only one package changed)
- Long build times because thereβs no coordination between tools
- Inconsistent project configuration spread across dozens of
package.jsonfiles - Manual dependency tracking (which projects depend on which?)
Nx solves these with:
- Dependency graph β Nx knows which projects depend on which
- Affected detection β run only what changed and its dependents
- Caching β never rebuild something that already built with the same inputs
- Generators β create consistent project scaffolding in seconds
Nx vs Turborepo
Both Nx and Turborepo are monorepo build tools with caching. Key differences:
| Feature | Nx | Turborepo |
|---|---|---|
| Task dependency model | Project graph + task graph | Pipeline configuration |
| Generators/scaffolding | Built-in, extensive | Not included |
| Framework support | React, Angular, Node, .NET | Framework-agnostic |
| Remote cache | Nx Cloud (free tier) | Vercel Remote Cache |
| Plugin ecosystem | Large (official + community) | Smaller |
| Visualization | nx graph with full UI | turbo run --graph (Mermaid) |
| Learning curve | Steeper | Simpler |
Choose Turborepo for: simple multi-package repos where you want minimal tool surface area. Choose Nx for: large monorepos with diverse projects, teams that want generators, or projects using Angular/React where Nxβs framework integration adds value.
Setting Up Nx
Creating a New Workspace
npx create-nx-workspace@latest my-workspace
The interactive setup asks:
- Preset (apps, npm, ts, react, angular, node, etc.)
- Whether to use Nx Cloud
For a React + Node monorepo:
npx create-nx-workspace@latest my-workspace \
--preset=ts \
--nxCloud=yes
Adding Nx to an Existing Monorepo
If you already have a monorepo (yarn/npm/pnpm workspaces):
npx nx@latest init
Nx analyzes your existing setup and configures itself minimally. You donβt need to restructure anything.
Workspace Structure
my-workspace/
βββ apps/
β βββ web/ # React app
β βββ api/ # Express API
β βββ mobile/ # React Native app
βββ libs/
β βββ ui/ # Shared UI components
β βββ data-access/ # Shared data layer
β βββ utils/ # Shared utilities
βββ nx.json # Nx configuration
βββ package.json # Root package.json
βββ tsconfig.base.json # Shared TypeScript config
The apps/ vs libs/ convention is standard Nx practice:
- apps: deployable applications, consume libraries
- libs: shared code, consumed by apps and other libs
nx.json Configuration
The core configuration file:
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"cache": true
},
"lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"cache": true
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/jest.config.[jt]s"
],
"sharedGlobals": ["{workspaceRoot}/babel.config.json"]
}
}
Key concepts:
- targetDefaults: default configuration applied to all projectsβ targets of that name
- dependsOn:
"^build"means βrun build in all dependencies firstβ - inputs: which files affect this targetβs cache
- cache: whether to cache outputs
Running Tasks
Basic Commands
# Run a target in a specific project
nx build web
nx test ui
nx lint api
# Run a target in all projects
nx run-many -t build
nx run-many -t test lint
# Run affected projects only (changed since main branch)
nx affected -t build
nx affected -t test
The Affected Command
nx affected is the most powerful feature for CI:
# Compared to main branch
nx affected -t build --base=main
# Compared to last commit
nx affected -t test --base=HEAD~1
# Compared to a specific commit
nx affected -t build --base=abc123
Nx analyzes the dependency graph and determines which projects are affected by the changed files. If you change a shared utility library, all projects that depend on it are marked as affected.
Parallel Execution
# Run up to 4 tasks in parallel
nx run-many -t build --parallel=4
# Nx automatically sequences tasks based on dependencies
# (builds dependencies before dependents)
Caching
How Nx Cache Works
For each task, Nx computes a cache key based on:
- The taskβs inputs (defined in
nx.json) - The projectβs configuration
- Environment variables you specify
If a matching cache entry exists, Nx restores the outputs without re-running the task. This works both locally (file system cache) and remotely (Nx Cloud).
Cache Configuration
// project.json (per-project config)
{
"name": "web",
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/web"
},
"cache": true
}
}
}
The outputs field tells Nx what to cache. Set it precisely β caching too much wastes space, too little misses cache hits.
Nx Cloud Remote Cache
With Nx Cloud, cache is shared across your entire team and CI:
# Connect to Nx Cloud
npx nx connect
This generates a token and updates nx.json. Now when one developer or CI run builds web, every other developer gets the cached output β no rebuild needed.
Nx Cloudβs free tier covers most small-to-medium teams (unlimited contributors, 500 GB cache space).
Verifying Cache Works
nx build web # First run: builds
nx build web # Second run: should say "cache hit"
Youβll see output like:
> nx run web:build [local cache]
β 1/1 dependent project tasks succeeded
ββββββββββββββββββββββββββββββββββββββββββ
> NX Successfully ran target build for project web
Nx read the output from the cache instead of running the command for 1 out of 1 tasks.
Generators
Generators create code consistently β project scaffolding, components, services, and anything else that follows a pattern.
Using Built-in Generators
# Generate a new React application
nx g @nx/react:app my-app
# Generate a new library
nx g @nx/js:lib my-lib
# Generate a React component in a library
nx g @nx/react:component my-component --project=my-lib
# Generate a React hook
nx g @nx/react:hook useMyHook --project=my-lib
Dry Run
Always preview what a generator will do:
nx g @nx/react:app my-app --dry-run
Shows all files that would be created/modified without touching anything.
Writing Custom Generators
// tools/generators/my-generator/index.ts
import { Tree, formatFiles, generateFiles, names } from '@nx/devkit'
import * as path from 'path'
interface MyGeneratorSchema {
name: string
directory?: string
}
export default async function (tree: Tree, options: MyGeneratorSchema) {
const normalizedOptions = {
...options,
...names(options.name), // camelCase, PascalCase, etc.
}
// Generate files from templates
generateFiles(
tree,
path.join(__dirname, 'files'),
`libs/${options.directory || options.name}`,
normalizedOptions
)
await formatFiles(tree)
}
Template files use EJS-style substitution:
// tools/generators/my-generator/files/__name__.tsx.template
export function <%= className %>() {
return <div><%= name %></div>
}
Run your custom generator:
nx g my-generator my-component --dry-run
Project Graph Visualization
One of Nxβs best features β visualize your dependency graph:
nx graph
Opens a browser with an interactive visualization showing:
- All projects and their relationships
- Which projects are affected by a change
- Dependency paths between projects
For CI, generate a static image:
nx graph --file=graph.html
Enforcing Module Boundaries
Nx can enforce rules about which projects can import from which:
// .eslintrc.json
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:ui", "type:data-access", "type:util"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui", "type:util"]
}
]
}
]
}
}
Tag your projects in project.json:
{
"name": "ui",
"tags": ["type:ui", "scope:shared"]
}
Now nx lint will fail if a ui library tries to import a feature library. This enforces clean architecture at the tooling level.
CI Integration
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for nx affected
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Set NX base and head for PR
uses: nrwl/nx-set-shas@v4
- name: Run affected builds
run: npx nx affected -t build --parallel=3
- name: Run affected tests
run: npx nx affected -t test --parallel=3
- name: Run affected lint
run: npx nx affected -t lint --parallel=3
The nrwl/nx-set-shas@v4 action sets NX_BASE and NX_HEAD environment variables so nx affected knows what to compare against.
Distributed Task Execution (Nx Cloud)
For large teams, Nx Cloud can distribute tasks across multiple CI agents:
- name: Start Nx agents
run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build,test,lint"
- name: Run affected
run: npx nx affected -t build test lint
Each CI agent picks up tasks from the queue. Your total CI time approaches the time of the longest single task, regardless of total task count.
Nx Plugins for Common Stacks
Nx has official plugins for the most common frameworks:
# React
npm install @nx/react
# Next.js
npm install @nx/next
# Node/Express
npm install @nx/node
# NestJS
npm install @nx/nest
# Angular
npm install @nx/angular
# Expo/React Native
npm install @nx/expo
# Storybook
npm install @nx/storybook
# Cypress
npm install @nx/cypress
# Playwright
npm install @nx/playwright
Install the plugins, then use their generators:
nx g @nx/next:app my-next-app
nx g @nx/nest:app my-api
nx g @nx/storybook:configuration my-lib
Real-World Workspace Example
Hereβs how a mature monorepo might be organized:
my-workspace/
βββ apps/
β βββ web/ # Next.js consumer app
β βββ admin/ # React admin dashboard
β βββ api/ # NestJS API
β βββ mobile/ # Expo app
βββ libs/
β βββ ui/ # Shared React components (type:ui)
β β βββ button/
β β βββ forms/
β βββ feature/ # Feature modules (type:feature)
β β βββ auth/
β β βββ payments/
β βββ data-access/ # API clients, stores (type:data-access)
β β βββ user/
β β βββ products/
β βββ utils/ # Pure utilities (type:util)
β βββ date/
β βββ validation/
βββ tools/
β βββ generators/ # Custom generators
βββ nx.json
In this structure:
uilibraries can import from otheruiandutilsfeaturelibraries can import fromui,data-access, andutilsappscan import from any libraryutilscan only import from otherutils
This creates a directed acyclic dependency graph with clean layer separation.
Key Takeaways
nx affected -t buildruns only what changed β the single most impactful CI optimization- Caching is automatic for cached targets β zero rebuilds of unchanged code
- Nx Cloud shares cache across developers and CI agents for free
- Generators enforce consistent project structure at scale
nx graphvisualizes the dependency graph for debugging and planning- Module boundary rules enforce architecture constraints at lint time
- Distributed task execution scales CI to handle hundreds of projects
- Turborepo is simpler; Nx is more powerful β choose based on your scale and needs
Start with npx create-nx-workspace and nx affected in CI. Add Nx Cloud for remote caching. Introduce module boundaries as your team grows. The investment pays off when your monorepo scales beyond a handful of projects.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.