Dev.to WebDev 🛠 Dev 👁 0 📖 15 min read

Angular Monorepos: When Does It Actually Make Sense?

Angular applications rarely stay small forever. A project starts with a single application, a few components, and some services. Over time, new requirements appear. Another application is introduced. Teams start sharing

Angular applications rarely stay small forever.

A project starts with a single application, a few components, and some services. Over time, new requirements appear. Another application is introduced. Teams start sharing components, models, and utilities.

Eventually, someone suggests:

"We should move everything into a monorepo and use Nx."

And honestly, that might be a good idea.

But is it always necessary?

Angular CLI already supports multiple projects in a workspace. Nx adds powerful capabilities for dependency management, task execution, caching, and CI optimization, but those capabilities come with additional concepts and tooling.

So, when does Nx actually make sense?

Rather than simply explaining what a monorepo is, I want to explore this through a practical example, compare Angular CLI with Nx, and understand what changes when a workspace grows.

The goal isn't to prove that Nx is better.

The goal is to understand when the benefits of Nx justify its additional complexity.

1. First, What Problem Are We Trying to Solve?

Let's imagine a company with two Angular applications:

  • An administrative dashboard.
  • A customer portal.

Both applications display information about vehicles, appointments, and operational status.

Initially, they are completely independent.

company/
├── admin-app/
└── customer-app/

Each application has its own dependencies, configuration, and deployment pipeline.

This is a perfectly reasonable architecture.

However, the applications eventually start sharing code:

  • UI components.
  • Domain models.
  • API clients.
  • Validation utilities.
  • Design system elements.

Now we have a few options:

  1. Duplicate the code.
  2. Create an internal npm package.
  3. Share code through a monorepo.
  4. Introduce a workspace tool such as Nx.

The important distinction is that sharing code and sharing a repository are two different decisions.

A shared npm package can work perfectly well across separate repositories. A monorepo becomes particularly interesting when applications need to coordinate their dependencies, development, testing, and releases.

Before introducing a new tool, we should understand which problem we are actually solving.

2. Angular CLI Already Supports Monorepos

This is something that is sometimes overlooked.

Angular CLI is not limited to a single application per repository.

It supports workspaces containing multiple applications and libraries.

Let's create one.

Creating an Angular workspace

ng new company-platform --no-create-application

cd company-platform

Now we can generate multiple applications:

ng generate application admin

ng generate application customer

And shared libraries:

ng generate library shared-ui

ng generate library shared-data

Our workspace now looks something like this:

company-platform/
├── projects/
│   ├── admin/
│   ├── customer/
│   ├── shared-ui/
│   └── shared-data/
├── angular.json
├── package.json
├── tsconfig.json
└── node_modules/

The exact generated files depend on the Angular version and project configuration, but the overall structure is familiar.

Angular CLI provides commands for building, testing, serving, and generating code across these projects.

For example:

ng serve admin

ng build customer

ng test admin

We can also build libraries:

ng build shared-ui

For many workspaces, this is already enough.

Sharing a component

Let's create a reusable vehicle card.

// projects/shared-ui/src/lib/vehicle-card/vehicle-card.component.ts

import { Component, input } from '@angular/core';

export interface Vehicle {
  id: string;
  registration: string;
  model: string;
}

@Component({
  selector: 'shared-vehicle-card',
  standalone: true,
  template: `
    <article>
      <h3>{{ vehicle().registration }}</h3>
      <p>{{ vehicle().model }}</p>
    </article>
  `,
})
export class VehicleCardComponent {
  vehicle = input.required<Vehicle>();
}

We can export it from the library's public API:

// projects/shared-ui/src/public-api.ts

export * from './lib/vehicle-card/vehicle-card.component';

Then the admin application can import it:

import { Component } from '@angular/core';

import { VehicleCardComponent } from 'shared-ui';

@Component({
  selector: 'app-vehicle-list',
  standalone: true,
  imports: [VehicleCardComponent],
  template: `
    @for (vehicle of vehicles; track vehicle.id) {
      <shared-vehicle-card [vehicle]="vehicle" />
    }
  `,
})
export class VehicleListComponent {
  vehicles: Vehicle[] = [];
}

The customer application can use the same library.

This is a legitimate monorepo setup.

And we haven't needed Nx yet.

If your main requirement is simply sharing Angular libraries between a few applications, Angular CLI might already solve your problem.

3. What Changes When We Introduce Nx?

Nx doesn't replace Angular.

It builds on the Angular ecosystem and adds capabilities for managing larger workspaces.

According to the official Nx comparison, Angular CLI already supports application generation, libraries, building, testing, and local development.

Nx adds capabilities such as:

  • First-class monorepo tooling.
  • Project dependency graphs.
  • Enforced module boundaries.
  • Affected task execution.
  • Task orchestration.
  • Local task caching.
  • Remote caching.
  • Distributed task execution.

These are the differences worth exploring.

Creating an Nx workspace

We can create a new Angular workspace with Nx:

npx create-nx-workspace@latest company-platform \
  --template=nrwl/angular-template

The Angular template is available through Nx's workspace creation workflow.

Alternatively, we can migrate an existing Angular CLI workspace incrementally.

From the existing workspace:

npx nx add @nx/angular

This installs the Angular plugin and configures Nx integration.

It's important to keep Nx package versions compatible with each other. Using nx add is the recommended way to install the plugin into an existing Nx workspace.

Documentation:

Generating applications and libraries

With Nx, we can generate applications:

nx generate @nx/angular:app apps/admin

nx generate @nx/angular:app apps/customer

And libraries:

nx generate @nx/angular:lib libs/shared-ui

nx generate @nx/angular:lib libs/shared-data

Nx also supports more specific libraries:

nx generate @nx/angular:lib libs/vehicles/feature-list

nx generate @nx/angular:lib libs/vehicles/data-access

nx generate @nx/angular:lib libs/vehicles/ui

The exact generator options depend on the Nx and Angular versions, so check the Angular generator reference when building a real workspace.

A possible structure:

company-platform/
├── apps/
│   ├── admin/
│   └── customer/
│
├── libs/
│   ├── shared/
│   │   ├── ui/
│   │   └── data-access/
│   │
│   └── vehicles/
│       ├── feature-list/
│       ├── data-access/
│       └── ui/
│
├── nx.json
├── package.json
└── tsconfig.base.json

This organization makes it easier to understand which libraries belong to which domain.

However, the folder structure alone isn't the most important difference.

The real value starts when Nx understands the relationships between projects.

4. Project Graph: Understanding Dependencies

Imagine our workspace has these projects:

admin
customer
shared-ui
vehicles-data-access
billing
reports

The applications depend on shared libraries:

admin
 ├── shared-ui
 └── vehicles-data-access

customer
 ├── shared-ui
 └── vehicles-data-access

As the workspace grows, these relationships become increasingly important.

A change to shared-ui might affect both applications.

A change to billing might affect only the administrative application.

Without a clear understanding of these dependencies, it's easy to lose track of what needs to be tested or rebuilt.

Nx builds a project graph that represents relationships between projects.

You can inspect it with:

nx graph

The interactive graph helps developers understand how applications and libraries depend on each other.

This becomes especially useful when a workspace contains dozens or hundreds of projects.

Angular CLI can manage multiple projects, but it doesn't provide the same first-class project graph visualization and dependency-aware workflow as Nx.

The distinction isn't that Angular CLI cannot organize projects.

It's that Nx makes relationships between projects a central part of the development workflow.

5. Enforcing Architectural Boundaries

This is one of the most interesting features of Nx.

Let's say we have two applications:

apps/
├── admin/
└── customer/

And some shared libraries:

libs/
├── shared/
│   ├── ui/
│   └── utils/
│
└── vehicles/
    ├── feature-list/
    └── data-access/

A reasonable dependency structure might be:

admin → vehicles/feature-list
admin → shared/ui

customer → vehicles/feature-list
customer → shared/ui

vehicles/feature-list → vehicles/data-access

vehicles/data-access → shared/utils

But what happens when someone starts importing application-specific code into a shared library?

For example:

// libs/shared/ui/src/lib/admin-widget.ts

import { AdminService } from
  '../../../../apps/admin/src/app/services/admin.service';

This is a serious architectural problem.

A shared library should not depend on a specific application.

The problem isn't merely that the import path is ugly.

The dependency direction is wrong.

The shared library now depends on an application that depends on the shared library.

This can create circular dependencies, difficult refactoring, and unpredictable build relationships.

Nx module boundary rules

Nx provides the @nx/enforce-module-boundaries ESLint rule.

We can use project tags to describe architectural roles.

For example:

{
  "name": "admin",
  "tags": ["scope:admin", "type:app"]
}

A shared UI library:

{
  "name": "shared-ui",
  "tags": ["scope:shared", "type:ui"]
}

A vehicles data-access library:

{
  "name": "vehicles-data-access",
  "tags": ["scope:vehicles", "type:data-access"]
}

We can then define dependency constraints in ESLint.

A simplified configuration might look like this:

// eslint.config.mjs

import nx from '@nx/eslint-plugin';

export default [
  {
    files: ['**/*.ts'],
    rules: {
      '@nx/enforce-module-boundaries': [
        'error',
        {
          depConstraints: [
            {
              sourceTag: 'type:app',
              onlyDependOnLibsWithTags: [
                'type:app',
                'type:feature',
                'type:ui',
                'type:data-access',
                'type:util',
              ],
            },
          ],
        },
      ],
    },
  },
];

This is an illustrative rule. A real workspace should define its constraints according to its architecture, and the exact ESLint configuration may vary with the Nx version and generated setup.

The important point is that Nx can help enforce architectural decisions instead of relying exclusively on code review.

This is a concrete benefit: Nx can help prevent certain dependency relationships from being introduced in the first place.

Angular CLI doesn't provide an equivalent first-class module-boundary enforcement feature.

6. The Performance Question: Is Nx Actually Faster?

This is where I think we need to be careful.

It's easy to find claims that Nx makes builds dramatically faster.

But performance depends on many factors:

  • Number of projects.
  • Number of dependencies.
  • Build configuration.
  • Test configuration.
  • Hardware.
  • CI environment.
  • Cache availability.
  • How much of the workspace changes.

A small Angular application might not see a meaningful improvement.

A large monorepo with many independent projects could benefit significantly.

Let's break down the relevant features.

6.1 Angular CLI already has caching

Before comparing Nx, we need to acknowledge something important.

Angular CLI already supports persistent disk caching.

For example:

ng build admin

Running the same build again can reuse cached work.

The cache is enabled by default for local development environments.

You can inspect its configuration:

ng cache info

And control it through the workspace configuration.

For example:

ng config cli.cache.environment all

Angular CLI cache documentation:

https://angular.dev/cli/cache

Therefore, it would be misleading to compare:

  • Angular CLI with caching disabled.
  • Nx with caching enabled.

That wouldn't be a fair comparison.

6.2 Nx adds task-level caching

Nx introduces caching at the task level.

A task might be:

nx build admin

Nx calculates a hash based on relevant inputs, including source files, dependencies, configuration, and other task inputs.

If the same task with the same relevant inputs has already completed, Nx can restore its cached outputs instead of executing the task again.

For example:

nx build admin

Run it again:

nx build admin

If the task is cacheable and its inputs haven't changed, Nx can restore the previous result.

This isn't simply caching Angular's compilation.

Nx can cache the results of different kinds of tasks, including builds, tests, linting, and other configured commands.

Documentation:

https://nx.dev/docs/features/cache-task-results

6.3 Running only affected projects

This is where Nx becomes particularly interesting.

Imagine a workspace with:

admin
customer
shared-ui
vehicles-data-access
billing
reports

A change to vehicles-data-access might affect:

vehicles-data-access
admin
customer

But it shouldn't necessarily require rebuilding:

billing
reports

Nx can determine which projects are affected by a change.

For example:

nx affected -t build

And:

nx affected -t test

These commands run the relevant tasks for affected projects.

This is particularly useful in CI, where running every build and test on every commit can become expensive.

The actual benefit depends on the dependency graph.

If every application depends on the same library, changing that library may affect nearly the entire workspace.

In that case, the number of skipped tasks will be smaller.

6.4 Parallel task execution

Nx can also run independent tasks in parallel.

For example:

nx run-many -t build

This runs build tasks across projects.

Nx's task graph determines which tasks can run concurrently and which depend on other tasks.

This can reduce total execution time when the workspace contains independent projects.

However, parallel execution also depends on available CPU and memory resources.

Running too many builds simultaneously can increase memory pressure and make the machine slower.

Parallelism is not automatically a performance improvement in every environment.

7. Remote Caching: A Different Kind of Benefit

Local caching is useful for individual developers.

Remote caching becomes particularly interesting for teams.

Imagine five developers working on the same workspace.

Developer A builds and tests a shared library.

Developer B later makes a change to an application that uses that library.

If the library's task inputs haven't changed, the previous result may be reusable.

Nx Cloud can share cache results between developers and CI environments.

This means the same task doesn't necessarily need to run repeatedly on every machine.

The workflow can look like this:

Developer A
    |
    | Build shared library
    v
Remote Cache
    |
    | Reuse result
    v
Developer B / CI

Nx Cloud documentation:

https://nx.dev/docs/features/ci-features/remote-cache

This can be valuable when:

  • Builds are expensive.
  • Many developers share the same workspace.
  • CI repeatedly executes similar tasks.
  • Projects have stable dependencies.

But remote caching introduces additional considerations:

  • Configuration.
  • Cache access and permissions.
  • Infrastructure or subscription costs, depending on the setup.
  • Correct task input and output configuration.

A cache is only useful when the system can safely determine that the previous result is valid.

8. Angular CLI vs Nx: A Practical Comparison

Let's summarize what we've discussed.

Capability Angular CLI Nx
Multiple Angular projects Yes Yes
Angular applications Yes Yes
Angular libraries Yes Yes
Shared components Yes Yes
Build, test, serve Yes Yes
Project dependency graph Limited compared with Nx First-class
Enforced module boundaries No equivalent built-in feature Yes
Affected project execution No equivalent first-class workflow Yes
Task-level caching Angular build cache Nx task cache
Remote caching Not built in Available through Nx Cloud
Distributed task execution Not built in Available through Nx Cloud
Additional tooling complexity Lower Higher

One detail matters here: Angular CLI and Nx are not mutually exclusive.

Nx integrates with Angular CLI builders and schematics.

You can continue using familiar Angular tooling while gradually introducing Nx features.

The official Nx comparison provides a more detailed overview.

9. Performance Benchmarks: Let's Measure Instead of Guessing

I think this is an important section to include in any serious comparison.

Rather than claiming that Nx is a certain percentage faster, we can create a reproducible benchmark.

Benchmark environment

Use a workspace with:

  • Two Angular applications.
  • Five shared libraries.
  • Unit tests for each application and library.
  • A production build configuration.
  • A fixed Node.js version.
  • A fixed Angular and Nx version.
  • The same machine and operating system.

Record the environment:

Operating System:
CPU:
RAM:
Node.js:
Package Manager:
Angular:
Nx:

Scenarios to measure

Scenario What we want to understand
Initial build Baseline execution time
Repeated build Cache effectiveness
Full workspace test Cost of running everything
Small application change Incremental build behavior
Shared library change Dependency impact
CI build Potential pipeline improvement
Remote cache hit Cross-machine reuse

Example benchmark commands

Angular CLI:

ng build admin
ng build customer

ng test admin
ng test customer

Nx:

nx build admin
nx build customer

nx test admin
nx test customer

Affected tasks:

nx affected -t build
nx affected -t test

For meaningful measurements, use the same workspace, equivalent build targets, and equivalent test coverage.

Important: Separate cold and warm runs

A cold run measures execution without reusable task results.

A warm run measures what happens when caches are available.

These are different scenarios and should not be mixed.

For each scenario:

  1. Run the task once.
  2. Record the initial execution.
  3. Run it again without changing inputs.
  4. Record the repeated execution.
  5. Repeat the experiment several times.
  6. Compare medians rather than relying on a single run.

For Nx, record whether the result came from:

  • Task execution.
  • Local cache.
  • Remote cache.

For Angular CLI, record the cache configuration and whether it was enabled.

Benchmark results template

Here is a table that could be included in the final article:

Scenario Angular CLI Nx Notes
Initial build TBD TBD Cold run
Repeated build TBD TBD Warm cache
Full test suite TBD TBD All projects
Small app change TBD TBD Incremental
Shared library change TBD TBD Affected projects
CI execution TBD TBD Pipeline comparison

I would only publish actual numbers after running the experiment.

If Nx is 20% faster in one scenario but only 3% faster in another, that's a much more interesting result than claiming it is universally faster.

It also makes the article more trustworthy.

10. When Does Nx Actually Make Sense?

Let's move away from features and focus on decisions.

Scenario 1: One Angular application

my-app/
├── src/
├── angular.json
└── package.json

Nx probably isn't necessary.

Angular CLI provides everything required to develop, test, and build the application.

Introducing Nx might add concepts without solving a meaningful problem.

Scenario 2: Two independent applications

company/
├── admin/
└── customer/

If these applications have:

  • Independent teams.
  • Independent releases.
  • Few shared dependencies.
  • Different lifecycles.

Separate repositories might be more appropriate.

A monorepo isn't automatically better simply because the applications belong to the same company.

Scenario 3: Multiple applications sharing a design system

company-platform/
├── apps/
│   ├── admin/
│   └── customer/
│
└── libs/
    └── shared-ui/

This is a reasonable monorepo candidate.

Angular CLI can support this structure.

Nx becomes more attractive if we need:

  • Architectural dependency rules.
  • Project graph visualization.
  • Selective builds and tests.
  • Better task orchestration.
  • Remote caching.

Scenario 4: A large workspace with many domains

company-platform/
├── apps/
│   ├── admin/
│   ├── customer/
│   └── operations/
│
└── libs/
    ├── vehicles/
    ├── appointments/
    ├── billing/
    ├── authentication/
    └── shared/

At this point, Nx can provide meaningful value.

The workspace needs more than a place to store projects.

It needs a way to understand dependencies, enforce architectural boundaries, and avoid unnecessary work.

Scenario 5: Microfrontends

Microfrontends introduce a different set of concerns.

Applications may share a repository but still need:

  • Independent deployment.
  • Independent ownership.
  • Runtime integration.
  • Separate release cycles.

A monorepo can support this arrangement, but it doesn't automatically solve microfrontend architecture.

Nx may help with workspace organization and CI, but deployment independence and runtime integration still require their own design.

11. A Practical Decision Checklist

Before adopting Nx, I would ask:

  • Do we have multiple applications or libraries that genuinely belong together?
  • Are dependencies between projects becoming difficult to understand?
  • Do we need architectural boundaries enforced automatically?
  • Are CI builds and tests executing unnecessary tasks?
  • Would task caching make a measurable difference?
  • Do we need to coordinate changes across applications?
  • Are the benefits worth introducing another tool into the development workflow?

If most answers are no, Angular CLI might be enough.

If several answers are yes, Nx deserves serious consideration.

The important part is not the number of applications.

It's the complexity of coordinating them.

12. My Take: Start Simple, Introduce Nx When the Problems Become Real

I don't think Nx should be the default answer to every Angular workspace.

Angular CLI is capable of managing multiple applications and libraries.

For smaller workspaces, that may be all you need.

But as a workspace grows, the problems change.

The challenge is no longer just creating applications or sharing components.

It becomes:

  • Understanding dependencies.
  • Enforcing architectural boundaries.
  • Avoiding unnecessary builds.
  • Coordinating tasks.
  • Improving CI performance.
  • Maintaining a consistent development workflow.

This is where Nx becomes valuable.

Not because it makes your architecture automatically better.

Not because every monorepo needs it.

But because it provides tooling for problems that become increasingly difficult to manage manually.

And that's the distinction I think matters most.

Don't introduce Nx because your project has multiple applications. Introduce it when the complexity of managing those applications becomes a problem that Nx can actually solve.

Final Thoughts

Monorepos are a strategy, not an architectural achievement.

Angular CLI already provides a solid foundation for organizing multiple applications and libraries.

Nx extends that foundation with project graphs, dependency constraints, task orchestration, caching, and CI optimization.

Those features can make a significant difference in larger workspaces.

But they also introduce additional concepts and configuration.

The best choice depends on the problems your team is facing today, not the scale you imagine your project might eventually reach.

I'd rather start with a simple workspace and introduce complexity when it becomes useful than build a sophisticated monorepo that solves problems we don't have.

What About Your Projects?

Have you adopted Nx in an Angular workspace?

Did it actually improve your development experience or CI performance?

Or did Angular CLI turn out to be enough?

I'm particularly interested in real-world experiences with affected builds, caching, and architectural boundaries.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.