| |

Angular 113 🅰️ Monorepos with Nx

⏱️ Estimated read: 16 min | |

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

CommandPurpose
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 testTest only affected projects
npx nx graphVisualize project dependencies
npx nx run-many -t buildBuild all projects
npx nx g @nx/angular:lib <name>Generate a library
npx nx connectEnable Nx Cloud remote caching

Workspace Structure

PathPurpose
apps/Deployable applications
libs/Shared and feature libraries
tools/Scripts and workspace utilities
nx.jsonWorkspace-wide Nx configuration
project.jsonPer-project task configuration
tsconfig.base.jsonPath mappings for library imports

Affected Command Options

FlagPurpose
--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

FlagEffect
(default)Read from local and remote cache
--skip-nx-cacheIgnore all cache, re-execute
--no-cloudSkip 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

PitfallWhy It HappensFix
nx affected runs everythingNo base commit or shallow cloneSet fetch-depth: 0 and configure base SHA
Cache never hitsInputs changing unnecessarilyCheck inputs in targetDefaults
Import fails after library creationLibrary not in path mappingsRegenerate or manually add to tsconfig.base.json
Module boundary violationTags not configured or ESLint not runAdd tags and enable @nx/enforce-module-boundaries
Build output missingWrong output path in project.jsonVerify outputs matches actual build location
Slow CI despite cachingRemote cache not connectedRun 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

ItemValue
Nx workspace creationnpx create-nx-workspace@latest --preset=angular-monorepo
Application locationapps/
Library locationlibs/
Per-project configproject.json
Workspace confignx.json
Dependency graphComputed from imports; view with nx graph
Affected commandnx affected -t <target>
Base/head in CInx-set-shas action or --base / --head flags
CachingLocal by default; remote via Nx Cloud
Module boundariesEnforced by @nx/enforce-module-boundaries
Tagsscope:* 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 affected runs 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: 0 and 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.


Was this helpful?


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!