Documentation Index Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
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. Think of Nx as a “project-aware” build system — it understands the dependency graph between your libraries and apps, so it can skip rebuilding things that have not changed. In a monorepo with 50 libraries, this means your CI might only need to test 3 of them on a given PR instead of all 50.
The core insight behind Nx : The biggest cost of a multi-repo setup is not the build tooling — it is the friction of sharing code across repos (npm publishing, version conflicts, diamond dependency problems). A monorepo eliminates this entirely. Every library is always at the latest version because it lives in the same source tree.
┌─────────────────────────────────────────────────────────────────────────┐
│ 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
Libraries are the fundamental unit of organization in an Nx monorepo. The key mental model: apps are thin shells that compose libraries . An app should contain almost no business logic — it just wires together feature libraries, provides configuration, and defines routes. This means your code is reusable by default: if you build a second app (say, an admin panel), it can import the same libraries without copying code.
The four library types below are not just organizational — they define a dependency hierarchy that Nx enforces via linting rules.
# 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
Module boundary rules are the “immune system” of your monorepo. Without them, any developer can import from any library, and within weeks your dependency graph becomes a tangled mess where changing one utility library breaks 15 feature modules. The @nx/enforce-module-boundaries rule uses the tags you assigned during library creation to enforce a strict dependency hierarchy at lint time — before code ever gets merged.
The rule of thumb : Dependencies flow downward through the layer hierarchy. Apps depend on features, features depend on UI + data-access + domain, and everyone depends on util. Nothing flows upward or sideways between unrelated features.
// .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
Caching is the single biggest performance win Nx provides. The concept: if the inputs to a task (source files, dependencies, configuration) have not changed since the last run, the output is identical — so Nx skips the task and replays the cached result. For a monorepo with 50 libraries, this can reduce a 30-minute CI pipeline to 3 minutes on a typical PR that touches only 2-3 libraries.
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