Angular Monorepos with NX: The Power of Modularity

header angular monorepo with rx
header angular monorepo with rx

Recently, I’ve been working on migrating an enterprise-level application—a long-term project that will undoubtedly require sustained effort and careful planning. Specifically, I’m responsible for the frontend and am tasked with designing the architectural foundation that will support the rest of the application. The application in question is incredibly complex, both in terms of the number of pages (approximately 3,000 😱) and the various customizations implemented over the years for different clients.

In such a complex scenario, building solid foundations is essential for the project’s success.

For the frontend, we chose Angular, a framework with many years of proven experience that, with its latest innovations, is reclaiming its position as one of the best web frameworks currently available.

Throughout my career, I’ve built several complex applications, but never one of this magnitude. So, how do you best organize such a project? My attention, mind, and heart immediately turned to NX, a tool specifically designed for organizing complex projects. This article isn’t meant to explore every aspect (that would require a book!), but rather to introduce NX to those who haven’t encountered it before.

What is Nx and when should you use it?

NX is a development toolkit that enables you to manage monorepo workspaces for JavaScript/TypeScript applications (in my case, Angular). Created by Nrwl, NX allows you to maintain multiple related projects within a single repository, facilitating code sharing and ensuring consistency across your entire codebase. Unlike a traditional frontend project (Angular, React, etc.) where everything is contained within a single project, in a monorepo you can split your code across multiple libraries and web applications (either separate or interconnected—in the latter case, we’re talking about microfrontends). What are the advantages?

  • Sharing: Libraries and components can be easily shared between applications without the complexity of managing separate npm packages.
  • Atomic Refactoring: Changes affecting multiple projects can be managed in a single commit
  • Complete visibility: Developers have a comprehensive view of the application ecosystem
  • Consistent dependency management: A single version of dependencies for all projects
  • Optimized CI/CD ottimizzato: Intelligent building and testing of only the projects affected by changes

NX enhances this with purpose-built tools like intelligent caching, dependency analysis, and targeted builds that make monorepo management both efficient and scalable.

Sounds like a silver bullet, right? But it’s not always the right choice. Here’s my rule of thumb:

when to use Nx:

  • Your project involves multiple related applications or has substantial scale
  • Code sharing between applications is a priority
  • You have multiple teams working concurrently on different system components
  • Quando desideri un’infrastruttura di testing e build coerente
  • You need consistent development practices and infrastructure across teams
  • Your project has significant growth potential

skip nx when:

  • You’re building a simple standalone application
  • When the additional complexity isn’t justified by the benefits
  • You’re working on a short-lived project or rapid prototype

Traditional Angular vs. NX: Architectural Differences

Let’s examine the main project-level differences between a project created with the Angular CLI versus one created with NX.

Workspace structure

Standard Angular:

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

Nx workspace:

my-workspace/
├── apps/
│   ├── my-app/
│   │   ├── src/
│   │   └── ...
│   └── my-other-app/
├── libs/
│   ├── shared-ui/
│   ├── feature-auth/
│   └── ...
├── nx.json
├── workspace.json (o project.json)
├── package.json
└── ...

We can already see that the NX workspace structure (that’s what the container for all projects is called) is designed to handle multiple projects. You might have domain-specific libraries and different applications drawing from these libraries, all within a single workspace.

Configuration files

In Nx, the main configuration files are:

  1. nx.json: Configures global workspace options
  2. project.json: Replaces the traditional angular.json for each project
  3. tsconfig.base.json: Contains base TypeScript configurations for the entire workspace. Each project will have its dedicated file that extends this base file.
  4. In an NX workspace, each application and library has its own project.json file defining its specific configurations. This modular approach allows for more granular project management.

Project Creation and Basic Commands

Getting started with NX is simple, but you should know it has its own set of commands. Just as we use the ng serve command with Angular CLI, with NX, we’ll need to use dedicated commands. Here are the main commands to get started:

Bash
#Global installation of Nx CLI
npm install -g nx

# Creating a new workspace
npx create-nx-workspace my-workspace-name

During setup, you’ll be asked some questions about your workspace configuration, and you can choose from various presets, including those for Angular.

Once you’ve created the workspace, here are some fundamental commands:

Bash
# Generate a new Angular application called my-app-name
nx generate @nx/angular:application my-app-name

# Generate a shared "shared-ui" library
nx generate @nx/angular:library shared-ui

# Run the application (equivalent to ng serve)
nx serve my-app

# Run tests
nx test my-app

# Run lint
nx lint my-app

# Build the application (equivalent to ng build)
nx build my-app

The Game-Changing NX Cache

One of the (many) advantages of NX is its caching system. When you run a command like nx build or nx test, the artifacts produced are stored locally. If you run the same command again without changing the code, NX will use the previously stored results instead of executing the operation again.

The cache works by calculating a hash based on:

  • The project’s source code
  • The project’s dependencies
  • The task configuration
  • The execution environment

You can manage the cache using these commands:

Bash
# Clear the cache
nx reset

# View cache status
nx report

This caching mechanism significantly accelerates development cycles, especially in large projects or CI/CD environments, by avoiding recompiling parts that haven’t changed. Additional capabilities are offered in the paid version of NX, called NX Cloud.

NX Tasks: Automation on a New Level

Tasks in NX represent operations you can perform on your projects, such as build, test, lint, etc. In a context where you might have dozens of projects, this allows you to define commands to run in specific situations, with the advantage that they can be executed in parallel.

Tasks can be defined in three different places: package.json, project.json, or nx.json.

Each task is defined by:

  • A name (like “build”)
  • An executor (the code that runs the task)
  • Configuration options

You can run tasks with nx run project-name:task-name or with shortcuts like nx build project-name.

A powerful aspect of NX tasks is the ability to define dependencies between them using dependsOn:

JSON
"build": {
  "executor": "@nx/angular:webpack-browser",
  "dependsOn": [
    "^build"
  ],
  "options": {...}
}

This example ensures all dependency projects are built before building the current project. Combined with caching, this means you’ll never waste time rebuilding unchanged components.

Libraries in NX: The Building Blocks of Your Monorepo

As the title of this section suggests, libraries are the building blocks used to construct our application. Large-scale projects involve dividing the project into multiple parts, which can then be used across multiple applications and allow different teams to work in parallel on different contexts.

The NX documentation identifies several types of libraries, though each team can decide on different organizational strategies:

  1. Feature Libraries: Contain specific business functionality, often tied to a particular feature of the application. For example, a library for authentication or order management.
  2. UI Libraries: Contain reusable UI components, such as buttons, forms, or entire layouts.
  3. Data-access Libraries: Manage data access, including services that communicate with APIs and application state management.
  4. Utility Libraries: Contain utility functions that can be used in different parts of the application.

Personally, I prefer organizing libraries into:

  • Core: Contains essential low-level elements for the entire application, from settings management to authentication. UI elements aren’t included here.
  • Shared: All elements that can be used across multiple modules, such as UI (buttons, reusable components, etc.).
  • Features: A dedicated library for each domain context (customer data, orders, shopping cart, etc.).

Library Implementation Types

Beyond categorization by functionality, NX allows for three distinct types of libraries:

  1. Simple: The default type with no dedicated build process—compiled alongside the applications that use them. Perfect for internal component libraries and domain-specific modules.
Bash
nx generate @nx/angular:library my-simple-lib
  1. Buildable: Independently compilable with their build targets. The resulting artifacts can be consumed by other libraries or applications, enabling better separation and faster testing.
Bash
nx generate @nx/angular:library my-buildable-lib --buildable
  1. Publishable: Extend buildable libraries with npm publishing capabilities. Ideal for code shared beyond your monorepo.
Bash
nx generate @nx/angular:library my-publishable-lib --publishable --importPath=@myorg/my-lib

Your choice depends on specific needs:

  • Use simple libraries for most internal components
  • Choose buildable libraries when optimizing test/build times for complex scenarios
  • Select publishable libraries for code shared outside your monorepo

This isn’t a permanent decision—you can start with simple libraries and evolve them as your project grows.

So, how do you create a library?

Bash
nx generate @nx/angular:library library-name --directory=auth

Module Boundaries: Architectural Guardrails

One of NX’s most powerful features is the ability to define and enforce dependency rules through “module boundaries.” These boundaries maintain architectural integrity by preventing circular dependencies and enforcing intended design patterns.

Imagine this scenario: your solution architect specifies that the Core library should have no external dependencies. Later, a developer attempts to import the Orders module into Core. With module boundaries, this violation is automatically flagged during development, preserving architectural integrity.

Solution architect vs Developer

Back to the topic, module boundaries are configured in nx.json or individual .eslintrc.json files:

JSON
{
  "extends": ["../../.eslintrc.json"],
  "ignorePatterns": ["!**/*"],
  "overrides": [
    {
      "files": ["*.ts"],
      "rules": {
        "@nx/enforce-module-boundaries": [
          "error",
          {
            "enforceBuildableLibDependency": true,
            "allow": [],
            "depConstraints": [
              {
                "sourceTag": "type:app",
                "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:utility"]
              },
              {
                "sourceTag": "type:feature",
                "onlyDependOnLibsWithTags": ["type:ui", "type:utility", "type:data-access"]
              },
              {
                "sourceTag": "type:ui",
                "onlyDependOnLibsWithTags": ["type:ui", "type:utility"]
              },
              {
                "sourceTag": "scope:admin",
                "onlyDependOnLibsWithTags": ["scope:shared", "scope:admin"]
              },
              {
                "sourceTag": "scope:customer",
                "onlyDependOnLibsWithTags": ["scope:shared", "scope:customer"]
              }
            ]
          }
        ]
      }
    }
  ]
}

Rules are defined by both type and scope:

  1. Type-based rules: Determine which types of libraries can depend on other types. For example, a UI library can only depend on other UI or utility libraries.
  2. Scope-based rules: Determine which libraries can depend on others based on their functional scope. For example, libraries with the “admin” scope can only depend on libraries with “shared” or “admin” scope.

To apply these rules, you assign tags to your projects in the project.json file:

JSON
{
  "name": "admin-dashboard",
  "tags": ["type:feature", "scope:admin"]
}

This way, if a developer tries to import a component from a library not allowed according to these rules, they’ll receive an error during lint, preventing unwanted dependencies.

Conclusion

NX provides a powerful framework for tackling the challenges of large-scale application development. It transforms how we build complex Angular applications by offering sophisticated tools to manage complexity, facilitate code sharing, and accelerate development cycles.

If you’re wrestling with a growing Angular application or managing multiple related applications, NX might be exactly the tool you need to elevate your development process.

The fundamental principle remains: complex problems require decomposition into manageable parts. Similarly, complex applications need architectures divided into coherent, well-bounded components.

See you soon!

Share this article
Shareable URL
Prev Post

What’s new in Angular 20

Next Post

Smart home automation: Building a resilient DIY system

Leave a Reply

Your email address will not be published. Required fields are marked *

Read next