Skip to content
Angular ng libraries 4 min read

Nx Monorepo

As Angular applications grow into multiple apps and shared libraries, the plain Angular CLI starts to strain: builds get slow, code-sharing turns ad hoc, and there is nothing stopping one feature from importing the internals of another. Nx is a build system and monorepo toolkit that solves exactly these problems. It understands the dependency graph between your projects, caches the results of every task, and ships first-class Angular generators so a workspace of dozens of apps and libraries stays fast, consistent, and enforceably modular.

Creating an Nx Angular workspace

You scaffold a new workspace with create-nx-workspace. Choosing the angular preset wires up an Angular application, the build executors, and the Nx plugins in one step.

npx create-nx-workspace@latest acme --preset=angular-monorepo \
  --appName=storefront --style=scss --standalone --e2eTestRunner=playwright

This produces a workspace with apps/storefront and a libs/ directory ready for shared code. To add more projects later, use the Angular plugin generators:

# A second deployable app
npx nx g @nx/angular:app apps/admin --standalone

# A buildable, publishable UI library
npx nx g @nx/angular:library libs/ui-button --buildable --standalone

Nx prefers small, focused libraries over a few large ones. More granular libraries mean a finer dependency graph, which means more cache hits and faster, more parallel builds.

The project graph

Nx derives a project graph by statically analysing your import statements plus configuration files. Every app and library is a node; every import is an edge. This graph drives caching, affected detection, and module-boundary enforcement. Explore it interactively:

npx nx graph

Because the graph is computed from real code, it never drifts out of sync with what your app actually imports — there is no manifest to maintain by hand.

Computation caching

Nx caches the output of any task it runs. The cache key is a hash of the project’s source, its dependencies’ source, the command, and relevant environment. If nothing that affects the result has changed, Nx replays the previous output instantly instead of re-running the task.

npx nx build storefront
# edit nothing, run again:
npx nx build storefront

Output:

> nx run storefront:build  [existing outputs match the cache, left as is]

Successfully ran target build for project storefront (15ms)

Nx read the output from the cache instead of running the command for 1 out of 1 tasks.

The same caching applies to test, lint, and e2e. Connecting the workspace to Nx Cloud shares this cache across your whole team and CI, so a build another developer already ran is free for you.

Running only what changed

In a large monorepo you rarely need to rebuild everything. The affected commands use the project graph to test or build only projects touched by a change set, compared against a base branch.

# Lint and test only projects affected since main
npx nx affected -t lint test --base=main --head=HEAD

# Build everything, in parallel, in graph order
npx nx run-many -t build --all --parallel=3
CommandWhat it does
nx <target> <project>Run one target for one project
nx run-many -t <target>Run a target across many projects
nx affected -t <target>Run a target only on graph-affected projects
nx graphVisualise the dependency graph
nx g <generator>Scaffold code with a generator

Enforcing module boundaries

The most valuable architectural feature is the @nx/enforce-module-boundaries ESLint rule. You tag each project, then declare which tags may depend on which. The linter fails the build when a rule is violated, so your intended architecture is enforced mechanically rather than by convention.

Tag a project in its project.json:

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

Then define the allowed dependencies in the root ESLint config:

// eslint.config.mjs (flat config)
export default [
  {
    rules: {
      '@nx/enforce-module-boundaries': [
        'error',
        {
          depConstraints: [
            { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:ui', 'type:data'] },
            { sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui'] },
            { sourceTag: 'scope:admin', onlyDependOnLibsWithTags: ['scope:admin', 'scope:shared'] },
          ],
        },
      ],
    },
  },
];

Now a type:ui library that tries to import a type:data service is a lint error. Combined with import aliases (@acme/ui-button), this keeps standalone components and signal-based stores cleanly layered.

Generators

Generators are code scaffolds that respect your conventions. They create standalone components, functional guards, and stores in the right place with the right boilerplate.

npx nx g @nx/angular:component libs/ui-button/src/lib/button --standalone
npx nx g @schematics/angular:guard libs/auth/src/lib/auth --functional

The generated component uses modern Angular by default:

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

@Component({
  selector: 'acme-button',
  standalone: true,
  template: `
    @if (disabled()) {
      <button disabled>{{ label() }}</button>
    } @else {
      <button>{{ label() }}</button>
    }
  `,
})
export class ButtonComponent {
  label = input.required<string>();
  disabled = input(false);
}

Best Practices

  • Split code into many small, single-purpose libraries rather than a handful of large ones — granularity is what makes caching and affected effective.
  • Tag every project with a scope: and a type: and enforce them with @nx/enforce-module-boundaries so architecture cannot erode silently.
  • Wire nx affected into CI against your default branch to cut pipeline time to only the work that actually changed.
  • Enable Nx Cloud to share the computation cache across developers and CI, turning repeated builds into instant cache replays.
  • Always scaffold with generators so naming, file layout, and standalone/signal conventions stay uniform across the workspace.
  • Run nx graph during reviews to confirm new dependencies match your intended architecture before they merge.
Last updated June 14, 2026
Was this helpful?