Nx Monorepo Overview
Estimated Time: 3 hours | Difficulty: Advanced | Prerequisites: Angular CLI, Build Process
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ 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
Copy
# 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
Copy
// 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
Copy
# 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
Copy
# 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
Copy
# 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// .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"]
}
]
}
]
}
}
]
}
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ 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
Copy
# 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
Copy
// 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
Copy
// nx.json
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"],
"cacheDirectory": ".nx/cache"
}
}
}
}
Nx Cloud (Remote Caching)
Copy
# 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
Copy
# .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
Copy
// ✅ 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