<link rel="stylesheet" href="assets/fonts/inter.css"> Expert NX Monorepo for Angular Design Systems:… | Viral Patel Studio Skip to main content

Expert NX Monorepo for Angular Design Systems: Enterprise Guide 2025

Dec 18, 2025By Viral Patel

Build a scalable Angular design system with an NX monorepo in 2025: workspace setup, publishable libraries, Storybook, and the NX release pipeline.

NX Monorepo for Angular Design Systems: Enterprise Guide 2025

NX Monorepo Architecture

learn about Angular

design system

design systems

angular

Proven Publishing an Angular Component Library to NPM: Step-by-Step Guide 2025 Figure 1: An NX monorepo lets you manage your Angular design system libraries, consumer apps, and Storybook in one unified workspace

Why NX for Your Angular Design System?

A design system at scale involves more than one package: a UI component library, a tokens library, a utility library, a testing helpers library, an icons library — and the Storybook documentation site. Managing these as separate repos means synchronized versioning nightmares, duplicate CI setup, and broken cross-library refactors.

NX solves this with a structured monorepo that understands your dependency graph, caches build artifacts, and only rebuilds what changed. For design system teams, it's the difference between a 40-minute CI run and a 4-minute one.


NX Monorepo vs Standard Angular Workspace

Feature Angular Workspace NX Workspace
Multiple apps Yes Yes
Multiple libraries Yes Yes
Incremental builds No Yes (computation caching)
Affected commands No Yes (nx affected)
Dependency graph visualization No Yes (nx graph)
Module boundary enforcement Manual ESLint rule, tags
Storybook integration Manual @nx/storybook generator
Release management Manual nx release
Remote caching No Nx Cloud

Step 1: Create the NX Workspace

npx create-nx-workspace@latest my-design-system \
  --preset=angular-monorepo \
  --appName=docs \
  --style=scss \
  --bundler=esbuild \
  --e2eTestRunner=none \
  --nxCloud=skip

This creates:

my-design-system/
  apps/
    docs/                 ← Storybook or documentation app
  libs/                   ← Design system libraries go here
  nx.json                 ← NX configuration
  tsconfig.base.json      ← Shared TypeScript config + path aliases

Step 2: Generate Design System Libraries

NX uses generators to create consistently-structured libraries. Here's the full library taxonomy for a complete design system:

# UI Components — the primary publishable library
nx generate @nx/angular:library ui \
  --directory=libs/ui \
  --publishable \
  --importPath=@my-ds/ui \
  --prefix=ds \
  --standalone \
  --style=scss

# Design Tokens — CSS/SCSS/TS token files
nx generate @nx/angular:library tokens \
  --directory=libs/tokens \
  --publishable \
  --importPath=@my-ds/tokens \
  --buildable

# Utility functions — shared helpers
nx generate @nx/angular:library utils \
  --directory=libs/utils \
  --publishable \
  --importPath=@my-ds/utils \
  --standalone

# Icons — SVG icon components
nx generate @nx/angular:library icons \
  --directory=libs/icons \
  --publishable \
  --importPath=@my-ds/icons \
  --standalone

# Testing helpers — test utilities and harnesses
nx generate @nx/angular:library testing \
  --directory=libs/testing \
  --publishable \
  --importPath=@my-ds/testing \
  --standalone

Library Type Reference

Library --publishable --buildable Purpose
ui ✓ — Angular components, published to NPM
tokens ✓ — Design tokens, published to NPM
utils ✓ — Shared utilities, published to NPM
icons ✓ — Icon components, published to NPM
testing ✓ — Test harnesses, internal + published
shared/types — ✓ Internal TypeScript interfaces

Step 3: Enforce Module Boundaries

NX's module boundary rule uses project tags to enforce architectural constraints. First, tag your projects in project.json:

// libs/ui/project.json
{
  "name": "ui",
  "tags": ["scope:design-system", "type:library"]
}

// apps/docs/project.json  
{
  "name": "docs",
  "tags": ["scope:docs", "type:app"]
}

// apps/consumer-app/project.json
{
  "name": "consumer-app",
  "tags": ["scope:app", "type:app"]
}

Then configure the ESLint rule in .eslintrc.json or eslint.config.js:

{
  "rules": {
    "@nrwl/nx/enforce-module-boundaries": ["error", {
      "allow": [],
      "depConstraints": [
        {
          "sourceTag": "scope:design-system",
          "onlyDependOnLibsWithTags": ["scope:design-system"]
        },
        {
          "sourceTag": "scope:app",
          "onlyDependOnLibsWithTags": ["scope:design-system", "scope:shared"]
        },
        {
          "sourceTag": "type:app",
          "notDependOnLibsWithTags": ["type:app"]
        }
      ]
    }]
  }
}

Now if any design system library tries to import from an app, the linter fails immediately — before it ever reaches code review.


Step 4: Set Up Storybook

NX has first-class Storybook support via the @nx/storybook plugin:

nx generate @nx/storybook:configuration ui \
  --uiFramework=@storybook/angular \
  --interactionTests=false

# Start Storybook for the ui library
nx storybook ui

NX generates .storybook/main.ts configured for the library:

// libs/ui/.storybook/main.ts
import type { StorybookConfig } from '@storybook/angular';

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-a11y',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/angular',
    options: {},
  },
};

export default config;

Write stories directly in the library folder, co-located with components:

libs/ui/src/lib/button/
  button.component.ts
  button.component.scss
  button.component.spec.ts
  button.stories.ts        ← co-located story

Step 5: Use Affected Commands in CI

This is where NX's ROI becomes undeniable. In your CI pipeline, replace full builds with affected builds:

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  affected:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Required for nx affected to compare to base

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - run: npm ci

      - name: Set NX base SHA
        uses: nrwl/nx-set-shas@v4

      - name: Lint affected
        run: npx nx affected --target=lint --parallel=3

      - name: Test affected
        run: npx nx affected --target=test --parallel=3 --configuration=ci

      - name: Build affected
        run: npx nx affected --target=build --parallel=3

      - name: Storybook build affected
        run: npx nx affected --target=build-storybook --parallel=2

nrwl/nx-set-shas@v4 automatically sets NX_BASE and NX_HEAD environment variables for correct affected computation on PRs.


Step 6: Publish Libraries with NX Release

NX Release (introduced in NX 17) handles versioning and publishing for all publishable libraries:

# Dry run — see what would be released
nx release --dry-run

# Release with automatic version bump (conventional commits)
nx release

# Release specific version
nx release version 2.0.0

# Publish to NPM after versioning
nx release publish

Configure release in nx.json:

{
  "release": {
    "projects": ["ui", "tokens", "utils", "icons"],
    "changelog": {
      "workspaceChangelog": {
        "createRelease": "github"
      }
    },
    "version": {
      "conventionalCommits": true
    }
  }
}

Conventional commits (feat:, fix:, chore:, BREAKING CHANGE:) drive automatic semantic version bumps — no manual version decisions.


Step 7: Visualize the Dependency Graph

NX builds a live dependency graph of your entire workspace:

nx graph

This opens an interactive browser visualization showing:

  • Which libraries depend on which
  • Which apps consume which libraries
  • The impact of a change in any node

For design systems, this is invaluable for understanding the blast radius of breaking changes before publishing.


Migrating an Existing Angular Workspace

Already have a multi-project Angular workspace? Add NX without breaking anything:

npx nx@latest init

NX detects your existing Angular projects, generates nx.json, and adds caching. Your existing ng commands continue working — NX is purely additive.


When NOT to Use NX

NX adds orchestration overhead. Don't use it if:

  • You have a single Angular app with one or two small utility libraries
  • Your team has fewer than 3 developers
  • You're building a proof of concept that will never scale
  • Your CI pipeline already caches effectively and runs in under 5 minutes

For a single-package design system, a standard Angular workspace with a standalone library project is simpler and faster to set up.

If your real driver is independent deployment per team rather than build speed, a monorepo alone will not give you that. See the Angular module federation step-by-step guide for how micro frontends fit into an Nx workspace.


The Payoff

A mature NX monorepo for a design system gives you:

  • Speed: Only build and test changed libraries — 80%+ CI time reduction is common
  • Safety: Module boundaries prevent architectural drift before it becomes technical debt
  • Consistency: NX generators enforce the same structure across all libraries
  • Visibility: The dependency graph makes impact analysis obvious
  • Automation: NX Release ties conventional commits directly to published NPM versions

Start with two libraries (UI + tokens) and add more as your system grows. NX scales gracefully from 2 libraries to 200.