Skip to main content
Nx Monorepo

Nx Monorepo Overview

Estimated Time: 3 hours | Difficulty: Advanced | Prerequisites: Angular CLI, Build Process
Nx is a smart, fast, and extensible build system with first-class support for Angular. It enables monorepo development, code sharing, and advanced tooling for large-scale applications.
┌─────────────────────────────────────────────────────────────────────────┐
│                    Nx Monorepo Structure                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   my-workspace/                                                          │
│   ├── apps/                        # Application projects                │
│   │   ├── web-app/                # Main Angular application            │
│   │   ├── admin-app/              # Admin application                   │
│   │   ├── mobile-app/             # Ionic/Capacitor app                 │
│   │   └── api/                    # NestJS backend                      │
│   │                                                                      │
│   ├── libs/                        # Shared libraries                    │
│   │   ├── shared/                                                        │
│   │   │   ├── ui/                 # Shared UI components                │
│   │   │   ├── util/               # Utility functions                   │
│   │   │   └── data-access/        # Shared API services                 │
│   │   ├── feature/                                                       │
│   │   │   ├── products/           # Products feature                    │
│   │   │   ├── cart/               # Cart feature                        │
│   │   │   └── auth/               # Auth feature                        │
│   │   └── domain/                                                        │
│   │       ├── user/               # User domain                         │
│   │       └── order/              # Order domain                        │
│   │                                                                      │
│   ├── tools/                       # Custom workspace tools              │
│   ├── nx.json                      # Nx configuration                   │
│   └── tsconfig.base.json          # Shared TypeScript config            │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Setting Up Nx Workspace

# Create new Nx workspace with Angular
npx create-nx-workspace@latest my-workspace --preset=angular-monorepo

# Add to existing Angular CLI project
npx nx@latest init

# Create workspace interactively
npx create-nx-workspace@latest

Workspace Configuration

// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
      "!{projectRoot}/tsconfig.spec.json",
      "!{projectRoot}/jest.config.[jt]s",
      "!{projectRoot}/.eslintrc.json"
    ],
    "sharedGlobals": []
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    },
    "e2e": {
      "inputs": ["default", "^production"],
      "cache": true
    }
  },
  "defaultBase": "main",
  "parallel": 3,
  "cacheDirectory": ".nx/cache"
}

Generating Applications & Libraries

Applications

# Generate Angular application
nx generate @nx/angular:application web-app \
  --style=scss \
  --routing \
  --standalone \
  --prefix=app

# Generate with specific options
nx g @nx/angular:app admin-app \
  --directory=apps/admin-app \
  --tags=scope:admin,type:app \
  --strict

# Generate NestJS API
nx g @nx/nest:application api \
  --directory=apps/api

Libraries

# Generate shared UI library
nx g @nx/angular:library ui \
  --directory=libs/shared/ui \
  --standalone \
  --buildable \
  --publishable \
  --importPath=@my-org/shared-ui \
  --tags=scope:shared,type:ui

# Generate feature library
nx g @nx/angular:library products \
  --directory=libs/feature/products \
  --standalone \
  --lazy \
  --routing \
  --tags=scope:products,type:feature

# Generate data-access library
nx g @nx/angular:library data-access \
  --directory=libs/shared/data-access \
  --standalone \
  --tags=scope:shared,type:data-access

# Generate utility library
nx g @nx/angular:library util \
  --directory=libs/shared/util \
  --standalone \
  --tags=scope:shared,type:util

# Generate domain library
nx g @nx/angular:library user \
  --directory=libs/domain/user \
  --standalone \
  --tags=scope:user,type:domain

Components & Services

# Generate component in library
nx g @nx/angular:component button \
  --project=shared-ui \
  --standalone \
  --export

# Generate service
nx g @nx/angular:service product \
  --project=feature-products

# Generate NgRx feature
nx g @nx/angular:ngrx products \
  --project=feature-products \
  --parent=libs/feature/products/src/lib/products.routes.ts \
  --route=products

Library Architecture

Shared UI Library

// libs/shared/ui/src/lib/button/button.component.ts
@Component({
  selector: 'shared-ui-button',
  standalone: true,
  imports: [CommonModule],
  template: `
    <button
      [class]="buttonClass()"
      [disabled]="disabled() || loading()"
      [type]="type()"
      (click)="handleClick($event)"
    >
      @if (loading()) {
        <shared-ui-spinner size="small" />
      }
      @if (icon() && iconPosition() === 'left') {
        <span class="icon">{{ icon() }}</span>
      }
      <ng-content />
      @if (icon() && iconPosition() === 'right') {
        <span class="icon">{{ icon() }}</span>
      }
    </button>
  `,
  styleUrl: './button.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ButtonComponent {
  variant = input<'primary' | 'secondary' | 'outline' | 'ghost'>('primary');
  size = input<'sm' | 'md' | 'lg'>('md');
  disabled = input(false);
  loading = input(false);
  type = input<'button' | 'submit' | 'reset'>('button');
  icon = input<string>();
  iconPosition = input<'left' | 'right'>('left');
  
  clicked = output<MouseEvent>();
  
  buttonClass = computed(() => 
    `btn btn-${this.variant()} btn-${this.size()}`
  );
  
  handleClick(event: MouseEvent) {
    if (!this.disabled() && !this.loading()) {
      this.clicked.emit(event);
    }
  }
}

// libs/shared/ui/src/index.ts
export * from './lib/button/button.component';
export * from './lib/card/card.component';
export * from './lib/modal/modal.component';
export * from './lib/table/table.component';
export * from './lib/form-field/form-field.component';
export * from './lib/spinner/spinner.component';

Feature Library

// libs/feature/products/src/lib/products.routes.ts
import { Routes } from '@angular/router';

export const PRODUCTS_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () => import('./products-list/products-list.component')
      .then(m => m.ProductsListComponent)
  },
  {
    path: ':id',
    loadComponent: () => import('./product-detail/product-detail.component')
      .then(m => m.ProductDetailComponent)
  },
  {
    path: ':id/edit',
    loadComponent: () => import('./product-edit/product-edit.component')
      .then(m => m.ProductEditComponent),
    canActivate: [adminGuard]
  }
];

// libs/feature/products/src/lib/products-list/products-list.component.ts
@Component({
  selector: 'feature-products-list',
  standalone: true,
  imports: [
    CommonModule,
    ButtonComponent,
    CardComponent,
    ProductCardComponent
  ],
  template: `
    <div class="products-page">
      <header class="page-header">
        <h1>Products</h1>
        <shared-ui-button 
          variant="primary"
          (clicked)="addProduct()"
        >
          Add Product
        </shared-ui-button>
      </header>
      
      @if (loading()) {
        <shared-ui-spinner />
      } @else {
        <div class="products-grid">
          @for (product of products(); track product.id) {
            <feature-product-card 
              [product]="product"
              (viewDetails)="viewProduct($event)"
            />
          }
        </div>
      }
    </div>
  `
})
export class ProductsListComponent {
  private store = inject(ProductStore);
  private router = inject(Router);
  
  products = this.store.products;
  loading = this.store.loading;
  
  ngOnInit() {
    this.store.loadProducts();
  }
  
  viewProduct(id: string) {
    this.router.navigate(['/products', id]);
  }
  
  addProduct() {
    this.router.navigate(['/products', 'new', 'edit']);
  }
}

Data Access Library

// libs/shared/data-access/src/lib/api.service.ts
@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);
  private config = inject(APP_CONFIG);
  
  get<T>(endpoint: string, params?: HttpParams): Observable<T> {
    return this.http.get<T>(`${this.config.apiUrl}${endpoint}`, { params });
  }
  
  post<T>(endpoint: string, body: unknown): Observable<T> {
    return this.http.post<T>(`${this.config.apiUrl}${endpoint}`, body);
  }
  
  put<T>(endpoint: string, body: unknown): Observable<T> {
    return this.http.put<T>(`${this.config.apiUrl}${endpoint}`, body);
  }
  
  patch<T>(endpoint: string, body: unknown): Observable<T> {
    return this.http.patch<T>(`${this.config.apiUrl}${endpoint}`, body);
  }
  
  delete<T>(endpoint: string): Observable<T> {
    return this.http.delete<T>(`${this.config.apiUrl}${endpoint}`);
  }
}

// libs/feature/products/src/lib/data-access/product.service.ts
@Injectable({ providedIn: 'root' })
export class ProductService {
  private api = inject(ApiService);
  
  getProducts(params?: ProductFilters): Observable<Product[]> {
    const httpParams = this.buildParams(params);
    return this.api.get<Product[]>('/products', httpParams);
  }
  
  getProduct(id: string): Observable<Product> {
    return this.api.get<Product>(`/products/${id}`);
  }
  
  createProduct(product: CreateProductDto): Observable<Product> {
    return this.api.post<Product>('/products', product);
  }
  
  updateProduct(id: string, updates: UpdateProductDto): Observable<Product> {
    return this.api.patch<Product>(`/products/${id}`, updates);
  }
  
  deleteProduct(id: string): Observable<void> {
    return this.api.delete(`/products/${id}`);
  }
  
  private buildParams(filters?: ProductFilters): HttpParams {
    let params = new HttpParams();
    if (filters?.category) {
      params = params.set('category', filters.category);
    }
    if (filters?.search) {
      params = params.set('search', filters.search);
    }
    return params;
  }
}

Module Boundaries

ESLint Configuration

// .eslintrc.json
{
  "root": true,
  "ignorePatterns": ["**/*"],
  "plugins": ["@nx"],
  "overrides": [
    {
      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
      "rules": {
        "@nx/enforce-module-boundaries": [
          "error",
          {
            "enforceBuildableLibDependency": true,
            "allow": [],
            "depConstraints": [
              {
                "sourceTag": "type:app",
                "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util", "type:data-access"]
              },
              {
                "sourceTag": "type:feature",
                "onlyDependOnLibsWithTags": ["type:ui", "type:util", "type:data-access", "type:domain"]
              },
              {
                "sourceTag": "type:ui",
                "onlyDependOnLibsWithTags": ["type:ui", "type:util"]
              },
              {
                "sourceTag": "type:data-access",
                "onlyDependOnLibsWithTags": ["type:util", "type:domain"]
              },
              {
                "sourceTag": "type:util",
                "onlyDependOnLibsWithTags": ["type:util"]
              },
              {
                "sourceTag": "type:domain",
                "onlyDependOnLibsWithTags": ["type:util"]
              },
              {
                "sourceTag": "scope:products",
                "onlyDependOnLibsWithTags": ["scope:products", "scope:shared"]
              },
              {
                "sourceTag": "scope:admin",
                "onlyDependOnLibsWithTags": ["scope:admin", "scope:shared"]
              }
            ]
          }
        ]
      }
    }
  ]
}
┌─────────────────────────────────────────────────────────────────────────┐
│              Module Boundary Rules                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   type:app ──────────► type:feature                                     │
│        │                   │                                             │
│        │                   ▼                                             │
│        │              type:ui ◄─────────────┐                           │
│        │                   │                 │                           │
│        │                   ▼                 │                           │
│        └──────────► type:data-access ───────┤                           │
│                           │                  │                           │
│                           ▼                  │                           │
│                      type:domain             │                           │
│                           │                  │                           │
│                           ▼                  │                           │
│                      type:util ◄─────────────┘                           │
│                                                                          │
│   Legend:                                                                │
│   ────► Can depend on                                                   │
│   ────X Cannot depend on (enforced by ESLint)                           │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Nx Commands

# View project graph
nx graph

# Run tasks
nx serve web-app
nx build web-app --configuration=production
nx test shared-ui
nx lint feature-products
nx e2e web-app-e2e

# Run affected (only changed projects)
nx affected:build
nx affected:test
nx affected:lint
nx affected -t build,test,lint

# Run in parallel
nx run-many -t build -p web-app,admin-app --parallel=3

# Reset cache
nx reset

# Show project info
nx show project web-app

# List projects
nx show projects --type=app
nx show projects --type=lib --with-target=build

Task Pipeline

// nx.json
{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "outputs": ["{options.outputPath}"],
      "cache": true
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["default", "^production"],
      "cache": true
    },
    "deploy": {
      "dependsOn": ["build", "test", "lint"],
      "cache": false
    }
  }
}

Caching & CI

Local Caching

// nx.json
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "lint", "test", "e2e"],
        "cacheDirectory": ".nx/cache"
      }
    }
  }
}

Nx Cloud (Remote Caching)

# Connect to Nx Cloud
npx nx connect-to-nx-cloud

# Or set access token
export NX_CLOUD_ACCESS_TOKEN=your-token

GitHub Actions with Nx

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

on:
  push:
    branches: [main]
  pull_request:

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - uses: nrwl/nx-set-shas@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      
      - run: npm ci
      
      - run: npx nx format:check
      
      - run: npx nx affected -t lint,test,build --parallel=3
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          directory: coverage/

Best Practices

Keep Libraries Small

Single responsibility - easier to test and maintain

Use Tags Consistently

Enforce boundaries with scope and type tags

Leverage Affected

Only build/test what changed for faster CI

Share via Libraries

Never import directly from apps
// ✅ Good - Import from library
import { ButtonComponent } from '@my-org/shared-ui';
import { ProductService } from '@my-org/feature-products';

// ❌ Bad - Import from app
import { ButtonComponent } from 'apps/web-app/src/components';

Next: Performance Optimization

Master advanced performance optimization techniques