Angular 113 🅰️ Monorepos with Nx
A monorepo places multiple applications and shared libraries in a single repository. For Angular teams, this structure solves a persistent problem: when several applications share authentication logic, UI components, or data models, duplicating that code across repositories creates drift and inconsistency. A change to a shared button component must be replicated in every repository that uses it. A bug fix in authentication logic must be applied everywhere. A monorepo eliminates this duplication by giving every project access to the same source code.
Nx is a build system designed specifically for monorepos, with first-class support for Angular . It extends the Angular CLI with features that make large workspaces practical: computation caching, affected-only task execution, enforced module boundaries, and distributed task execution through Nx Cloud. Where a single angular.json file becomes unwieldy at scale, Nx splits configuration into per-project project.json files and adds a project graph that understands dependencies between every project .
This chapter covers creating an Nx workspace for Angular, organizing applications and libraries, using nx affected to run tasks only on changed projects, enforcing architectural boundaries with tags, and setting up caching and CI acceleration.
Key point: Nx scales Angular monorepos by computing a project graph from your code, running tasks only on affected projects, caching every computation, and enforcing module boundaries that prevent architectural decay.
Why Nx exists for Angular monorepos
The scale problem. A single Angular application with one angular.json works fine at small scale. But when a repository contains five applications and twenty libraries, a monolithic configuration file becomes a bottleneck. Running ng test executes every test in every project. Running ng build builds everything, even when only one library changed. The Angular CLI was designed for single-application projects, not for the coordination of dozens of interdependent projects .
The affected problem. Without a tool that understands dependencies, a CI pipeline must either run all tasks or rely on manual configuration to determine what changed. Manual configuration is error-prone: developers forget to update build scripts when dependencies shift, and the result is either wasted computation or missed failures. Nx solves this by building a project graph from the code itself, using static analysis to determine which projects depend on which .
The caching problem. Builds and tests are deterministic given the same inputs. If source files and dependencies have not changed, the result of a build will be identical. Nx computes a hash of the task’s inputs—source files, dependency versions, environment variables—and stores the output in a local cache. Re-running the same task restores the cached output instantly . This turns a five-minute test run into a five-second cache restore when nothing changed.
The architectural drift problem. In a monorepo without constraints, developers import code directly from other applications, creating coupling that makes future changes difficult. A feature library imports from an application’s internal folder. A UI library reaches into another library’s private files. Nx provides module boundary rules that enforce import restrictions using project tags, preventing the organic decay that makes monorepos unmaintainable .
The CI cost problem. Running the full test suite and build for every pull request is wasteful. A one-line change to a utility library does not require rebuilding applications that do not depend on it. nx affected determines the minimum set of projects that could be affected by a change and runs tasks only on those, dramatically reducing CI time and compute cost .
a. Creating an Nx workspace for Angular
Nx workspaces are created with the create-nx-workspace command, which presents an interactive prompt for preset selection. Choosing the Angular preset configures the workspace with Angular, the @nx/angular plugin, and default settings for caching and task execution .
The resulting workspace structure separates applications and libraries into distinct folders. Applications live in apps/, libraries in libs/, and tools and configuration at the root . This separation is conventional rather than enforced by the filesystem, but it reinforces the distinction between deployable artifacts and reusable code.
Each project has its own project.json file that defines its targets: build, test, lint, serve, and any custom targets. The root nx.json contains workspace-wide configuration, including targetDefaults that apply to every project of a given type . This per-project configuration is more maintainable at scale than a single monolithic angular.json, because changes to one project’s configuration do not risk affecting others.
Creating a new application uses the Angular plugin’s generator:
npx nx g @nx/angular:app apps/my-app
Creating a library uses the library generator, with options for standalone components and the desired testing framework:
npx nx g @nx/angular:lib libs/shared/ui
Libraries created this way are automatically added to the TypeScript path mappings in tsconfig.base.json, so they can be imported by name from any project in the workspace . The library’s public API is defined by its index.ts, and only what is exported there is accessible to other projects.
b. The project graph and affected commands
Nx builds a project graph by analyzing import statements across the workspace. When a source file imports from a library, Nx records that dependency. When a library imports from another library, that edge is added to the graph. The graph is not configured manually; it is computed from the code .
This graph is the foundation for nx affected. The command works in three steps. First, it uses Git to determine which files changed between the base and head commits. Second, it maps those files to the projects that contain them. Third, it walks the project graph to find every project that depends on the changed projects. The result is the minimum set of projects that could be affected by the change .
Running nx affected -t test executes tests only for that affected set. If a change touches a utility library used by three applications, the tests for those three applications run. If a change touches an application that nothing depends on, only that application’s tests run.
In CI, the base commit should be set to the last successful run on the main branch, not simply the merge-base with main. The nx-set-shas GitHub Action automates this, fetching the correct base and head SHAs from the GitHub API . Without full Git history (fetch-depth: 0), the affected command cannot determine what changed, and it will fail or fall back to running everything.
c. Caching and remote caching
Every task in Nx is cacheable by default. Nx computes a hash of the task’s inputs: the source files of the project, the versions of dependencies, relevant environment variables, and the configuration of the task itself. If the hash matches a previous run, Nx restores the cached output instead of re-executing the task .
The cache is stored locally by default, in node_modules/.cache/nx. For CI, remote caching shares the cache across machines and team members. Nx Cloud provides remote caching as a hosted service. When a developer on one machine runs a build, the result is uploaded to the remote cache. When CI runs the same build, it downloads the cached result instead of rebuilding .
Remote caching transforms the economics of CI. A pull request that changes only a documentation file can have its entire test and build suite satisfied from cache in seconds. A pull request that changes a library used by three applications runs only those three applications’ tasks, and if those tasks were cached from a previous run on a similar change, they restore instantly.
The --skip-nx-cache flag forces a task to execute regardless of cache state, useful for verifying that a build actually works when you suspect the cache is masking a problem . The --no-cloud flag disables only remote caching, allowing local cache hits while skipping the network round-trip.
d. Module boundaries and project tags
Without constraints, monorepo imports become tangled. An application imports a library’s internal service directly instead of using its public API. A feature library imports from another feature library, creating cross-feature coupling. Over time, the dependency graph becomes a web that makes refactoring dangerous and incremental builds ineffective.
Nx enforces boundaries through project tags and ESLint rules. Each project is assigned tags in its project.json or package.json, typically reflecting its scope and type. A common scheme uses two dimensions: scope (scope:shop, scope:api, scope:shared) and type (type:feature, type:ui, type:data, type:util) .
The @nx/enforce-module-boundaries ESLint rule reads these tags and validates every import against configured constraints. A typical constraint might specify that type:ui libraries can import type:util libraries but not type:feature libraries, or that scope:shop libraries cannot import scope:api libraries . When a developer attempts a prohibited import, ESLint reports an error before the code is committed.
This enforcement is what keeps a monorepo’s dependency graph acyclic and its build times predictable. Without it, a single import can create a cycle that forces every project to rebuild when any project changes.
e. Distributed task execution and CI integration
For large workspaces where even affected tasks take too long on a single machine, Nx Cloud’s distributed task execution splits the work across multiple agents. Nx analyzes the task graph, determines the optimal division of labor based on available machines, and distributes tasks accordingly . Each agent runs its assigned tasks and reports results back to the coordinating process.
The CI workflow for an Nx Angular monorepo typically includes four steps: checkout with full Git history, set the base and head SHAs, run nx affected for lint, test, and build, and optionally deploy. The nx affected command becomes the primary entry point, replacing separate per-project scripts .
Nx also provides nx fix-ci for self-healing CI. When a pipeline fails due to common issues—missing dependencies, incorrect task configuration, cache invalidation—the command analyzes the failure and suggests or applies fixes directly in the pull request . This reduces the manual debugging that otherwise consumes developer time.
Complete Example Session
# ============================================
# PART 1: CREATE AN NX WORKSPACE
# ============================================
# Interactive prompt selects the Angular preset.
npx create-nx-workspace@latest myorg --preset=angular-monorepo
# ============================================
# PART 2: EXPLORE THE WORKSPACE STRUCTURE
# ============================================
# Apps and libs are separated by convention.
cd myorg
ls
# apps/ libs/ nx.json package.json tsconfig.base.json
# ============================================
# PART 3: SERVE THE DEFAULT APPLICATION
# ============================================
# Nx uses its own CLI, which wraps Angular.
npx nx serve shop
# Application available at http://localhost:4200
# ============================================
# PART 4: CREATE A SHARED UI LIBRARY
# ============================================
# Libraries hold reusable components and logic.
npx nx g @nx/angular:lib shared/ui
# ============================================
# PART 5: IMPORT THE LIBRARY IN AN APP
# ============================================
# The library is mapped in tsconfig.base.json.
# In apps/shop/src/app/app.ts:
import { UiComponent } from '@myorg/shared/ui';
# ============================================
# PART 6: VISUALIZE THE PROJECT GRAPH
# ============================================
# Nx computes dependencies from imports.
npx nx graph
# Opens a browser showing project nodes and dependency edges
# ============================================
# PART 7: RUN AFFECTED TASKS
# ============================================
# Only projects touched by the change run.
npx nx affected -t test
# Runs tests only for changed projects and their dependents
# ============================================
# PART 8: CONFIGURE MODULE BOUNDARIES
# ============================================
# Add tags to projects and configure ESLint rules.
# In libs/shared/ui/project.json:
{
"tags": ["scope:shared", "type:ui"]
}
# ============================================
# PART 9: ENABLE REMOTE CACHING
# ============================================
# Connect to Nx Cloud for shared cache.
npx nx connect
# Follow prompts to link workspace to Nx Cloud
# ============================================
# PART 10: CI WITH AFFECTED AND CACHING
# ============================================
# GitHub Actions workflow uses nx affected.
# .github/workflows/ci.yml
# - uses: actions/checkout@v4
# with: { fetch-depth: 0 }
# - run: npx nx affected -t lint test build
These ten parts cover workspace creation, project structure, serving applications, creating libraries, importing between projects, visualizing dependencies, running affected tasks, enforcing boundaries, enabling remote caching, and CI integration. Each step builds on the project graph that Nx computes from the code itself.
Quick Reference
Essential Nx Commands
| Command | Purpose |
|---|---|
npx nx serve <app> | Serve an application |
npx nx build <app> | Build a project |
npx nx test <lib> | Run unit tests |
npx nx affected -t test | Test only affected projects |
npx nx graph | Visualize project dependencies |
npx nx run-many -t build | Build all projects |
npx nx g @nx/angular:lib <name> | Generate a library |
npx nx connect | Enable Nx Cloud remote caching |
Workspace Structure
| Path | Purpose |
|---|---|
apps/ | Deployable applications |
libs/ | Shared and feature libraries |
tools/ | Scripts and workspace utilities |
nx.json | Workspace-wide Nx configuration |
project.json | Per-project task configuration |
tsconfig.base.json | Path mappings for library imports |
Affected Command Options
| Flag | Purpose |
|---|---|
--base=<sha> | Base commit for comparison |
--head=<sha> | Head commit for comparison |
--files=<paths> | Explicit changed files |
--parallel=<n> | Run tasks in parallel |
Cache Control
| Flag | Effect |
|---|---|
| (default) | Read from local and remote cache |
--skip-nx-cache | Ignore all cache, re-execute |
--no-cloud | Skip remote cache, allow local |
Best Practices
✅ Do This:
npx nx affected -t lint test build # CI runs only what changed
npx nx graph # Visualize dependencies
# project.json tags: ["scope:shared", "type:ui"] # Define boundaries
fetch-depth: 0 # Full git history for affected
❌ Don’t Do This:
ng test # ❌ Runs all tests, not affected
npx nx run-many -t build --all # ❌ Wastes time when only one changed
# No tags on projects # ❌ Boundaries cannot be enforced
fetch-depth: 1 # ❌ Affected command fails
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
nx affected runs everything | No base commit or shallow clone | Set fetch-depth: 0 and configure base SHA |
| Cache never hits | Inputs changing unnecessarily | Check inputs in targetDefaults |
| Import fails after library creation | Library not in path mappings | Regenerate or manually add to tsconfig.base.json |
| Module boundary violation | Tags not configured or ESLint not run | Add tags and enable @nx/enforce-module-boundaries |
| Build output missing | Wrong output path in project.json | Verify outputs matches actual build location |
| Slow CI despite caching | Remote cache not connected | Run nx connect and configure Nx Cloud |
Real-World Examples
1. Generate a Feature Library
npx nx g @nx/angular:lib feature-products --tags="scope:shop,type:feature"
2. Run Affected Tests in CI
- run: npx nx affected -t test --parallel=3
3. Visualize Only Affected Projects
npx nx graph --affected
4. Deploy Affected Applications
npx nx affected -t deploy --base=main
5. Enforce Module Boundaries
// .eslintrc.json
"@nx/enforce-module-boundaries": ["error", {
"depConstraints": [
{ "sourceTag": "type:feature", "onlyDependOnLibsWithTags": ["type:ui", "type:data"] }
]
}]
6. Add a Custom Deploy Target
// project.json
"deploy": {
"dependsOn": "build",
"command": "netlify deploy --dir=dist/apps/shop"
}
7. Skip Cache for Verification
npx nx build shop --skip-nx-cache
8. Connect Remote Cache
npx nx connect
9. Run Multiple Targets in Parallel
npx nx run-many -t lint test build --parallel=3
10. Show Project Details
npx nx show project shop --web
Visual
Nx Project Graph
┌──────────────────────────────────────────────────────────────┐
│ NX PROJECT GRAPH: COMPUTED FROM IMPORTS │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ shop │────────▶│feature- │────────▶│ shared- │ │
│ │ (app) │ │products │ │ ui │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ data │◀────────│ models │ │ util │ │
│ │ (lib) │ │ (lib) │ │ (lib) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ Arrows show import dependencies. Changing "models" marks │
│ "data" as affected, which marks "feature-products", which │
│ marks "shop". The affected set includes all dependents. │
└──────────────────────────────────────────────────────────────┘
Affected Command Workflow
┌──────────────────────────────────────────────────────────────┐
│ HOW nx affected DETERMINES WHAT TO RUN │
│ │
│ 1. Git diff: base...head │
│ └── Files changed: libs/models/src/product.ts │
│ │
│ 2. Map files to projects │
│ └── Project: @org/models │
│ │
│ 3. Walk project graph │
│ └── @org/models is imported by: │
│ @org/data │
│ @org/feature-products │
│ @org/shop │
│ │
│ 4. Run tasks on affected set │
│ └── nx affected -t test │
│ runs tests for models, data, feature-products, shop │
│ does NOT run tests for orders, inventory, etc. │
└──────────────────────────────────────────────────────────────┘
Module Boundary Enforcement
┌──────────────────────────────────────────────────────────────┐
│ TAGS AND CONSTRAINTS PREVENT ARCHITECTURAL DECAY │
│ │
│ Project tags: │
│ ├── shop: ["scope:shop", "type:app"] │
│ ├── feature: ["scope:shop", "type:feature"] │
│ ├── ui: ["scope:shared", "type:ui"] │
│ └── util: ["scope:shared", "type:util"] │
│ │
│ Constraint rules: │
│ ├── type:feature → can import type:ui, type:util │
│ ├── type:ui → can import type:util │
│ └── type:util → can import nothing (leaf) │
│ │
│ Violation example: │
│ feature-products imports from shop/src/internal/service.ts │
│ └── ESLint error: "A project tagged with type:feature │
│ cannot depend on a project tagged with type:app" │
│ │
│ The boundary is enforced at lint time, before commit. │
└──────────────────────────────────────────────────────────────┘
Caching Layers
┌──────────────────────────────────────────────────────────────┐
│ LOCAL AND REMOTE CACHE │
│ │
│ Developer machine: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Local cache: node_modules/.cache/nx │ │
│ │ Runs nx build → hash matches → restore instantly │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ upload on miss │
│ ▼ │
│ Nx Cloud: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Remote cache shared across team and CI │ │
│ │ CI runs nx affected → hash matches → download │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ download on miss │
│ ▼ │
│ CI runner: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Local cache populated from remote │ │
│ │ Subsequent runs on same runner hit local cache │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Nx workspace creation | npx create-nx-workspace@latest --preset=angular-monorepo |
| Application location | apps/ |
| Library location | libs/ |
| Per-project config | project.json |
| Workspace config | nx.json |
| Dependency graph | Computed from imports; view with nx graph |
| Affected command | nx affected -t <target> |
| Base/head in CI | nx-set-shas action or --base / --head flags |
| Caching | Local by default; remote via Nx Cloud |
| Module boundaries | Enforced by @nx/enforce-module-boundaries |
| Tags | scope:* and type:* conventions |
Key takeaways:
- Nx scales Angular monorepos beyond what the Angular CLI supports alone. It adds a project graph, caching, affected-only task execution, and module boundary enforcement that become essential as the number of projects grows .
- The project graph is computed from imports, not configured manually. Nx analyzes source files to determine which projects depend on which, enabling accurate affected calculation without developer intervention .
nx affectedruns tasks only on changed projects and their dependents. In CI, this reduces the compute and time spent on unrelated projects, with the savings proportional to the size and structure of the workspace .- Caching turns deterministic tasks into instant restores. Every task’s inputs are hashed, and matching outputs are restored from local or remote cache. Remote caching through Nx Cloud shares results across machines and CI .
- Module boundaries prevent architectural decay. Project tags and ESLint rules enforce which projects can import from which, keeping the dependency graph acyclic and the workspace maintainable .
- CI must use full Git history and a correct base SHA. The affected command compares the current state against a base commit; without
fetch-depth: 0and a proper base, it cannot determine what changed . - Distributed task execution extends caching to compute distribution. Nx Cloud can split tasks across multiple machines, reducing wall-clock time for large workspaces beyond what caching alone achieves .
Remember: An Nx monorepo is not just a folder with multiple Angular projects. It is a workspace where the build system understands the relationships between projects, runs only what is necessary, and enforces the architectural constraints that keep a large codebase maintainable. The project graph is the foundation: it powers affected calculation, caching, and boundary enforcement. Applications and libraries are separated by convention, configured per project, and connected by imports that Nx analyzes automatically. When you run nx affected, you are asking Nx to use Git to determine what changed and the project graph to determine what that change affects. When you configure tags and module boundaries, you are encoding architectural intent into the build system itself, making it difficult to introduce coupling that would slow future development. Nx is the tool that makes monorepos practical for Angular at scale.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!