Skip to main content

Lab 26: NX β€” Monorepo Architecture

πŸ“– Resources​

πŸš€ Starter Code​

No starter β€” you will create a fresh NX workspace from scratch.

In this lab you will set up an NX monorepo, generate two Angular applications, create a shared component library, and use the path alias to consume it. You will also explore the dependency graph and the affected command.

πŸ“ Instructions​

Step 1: Create the NX Workspace​

npx create-nx-workspace@latest my-workspace

When prompted:

  • Stack: Angular
  • Type: Integrated Monorepo
  • Application name: shop
  • Stylesheet format: CSS (or SCSS)
  • Enable distributed caching (Nx Cloud): No
my-workspace/
β”œβ”€β”€ apps/
β”‚ └── shop/
β”œβ”€β”€ libs/
β”œβ”€β”€ nx.json
β”œβ”€β”€ tsconfig.base.json
└── package.json

Step 2: Serve the App​

cd my-workspace
nx serve shop

Open http://localhost:4200 β€” the default Angular app is running.

Step 3: Generate a Second Application​

nx g @nx/angular:app admin

Now you have two independent apps sharing the same repository:

apps/
β”œβ”€β”€ shop/
└── admin/

Run each independently:

nx serve shop    # http://localhost:4200
nx serve admin # http://localhost:4201

Step 4: Generate a Shared UI Library​

nx g @nx/angular:lib ui --directory=libs/ui

Note (Nx v22+): Pass ui as the positional name and --directory=libs/ui as the full output path. Avoid combining --name=ui and --directory=libs/ui together β€” Nx treats --directory as the complete path, so the two flags can produce a nested libs/ui/ui/ instead of the intended libs/ui/.

Why libs/ and not src/?

A standard Angular project keeps everything inside src/. In an Nx Integrated Monorepo the convention is to place shareable code at the root libs/ folder so multiple apps (shop, admin, …) can all import from it without circular-path hacks. Each library is an independent TypeScript project with its own project.json.

libs/ui vs libs/shop/shared-ui

PathScopeUse when…
libs/ui/Workspace-wideGeneric UI primitives (buttons, icons, typography) used by any app
libs/shop/shared-ui/Shop scope onlyComponents specific to the shop domain that should not bleed into other scopes

Use libs/ui for building blocks that could belong to a design system. Use libs/shop/shared-ui for cart widgets, product cards, or anything shop-specific.

Check the path alias Nx generated

After the generator runs, open tsconfig.base.json and look for the new entry under paths:

{
"compilerOptions": {
"paths": {
"@my-workspace/ui": ["libs/ui/src/index.ts"]
}
}
}

This is how TypeScript resolves @my-workspace/ui to the actual source files in libs/ui/src/index.ts. No webpack alias or custom resolver is needed β€” it is standard TypeScript path mapping.

⚠️ The prefix depends on the org name entered during create-nx-workspace. If you chose acme, the alias will be @acme/ui. Always read tsconfig.base.json β†’ compilerOptions.paths to find the exact alias for your workspace before using it in imports.

Tags and module boundaries

Nx enforces import rules via @nx/enforce-module-boundaries in eslint.config.mjs. Libraries must carry tags so the rule knows which scopes may import them.

Open libs/ui/project.json and verify (or add) the tags field:

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

Without scope:shared, ESLint will block any app from importing this library once depConstraints are configured. You can inspect the boundary rules in eslint.config.mjs:

// eslint.config.mjs (excerpt)
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{ "sourceTag": "scope:shop", "onlyDependOnLibsWithTags": ["scope:shop", "scope:shared"] },
{ "sourceTag": "scope:admin", "onlyDependOnLibsWithTags": ["scope:admin", "scope:shared"] }
]
}
]

This means shop may only import libs tagged scope:shop or scope:shared. Tag libs/ui with scope:shared so both apps can use it freely.

Step 5: Create a Shared Component​

Generate a button component inside the library:

nx g @nx/angular:component libs/ui/src/lib/button/button --export

Note (Nx v22+): The --project flag has been removed from the component generator. Pass the full path to where the component should be created instead.

Edit libs/ui/src/lib/button/button.component.ts:

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

@Component({
selector: 'lib-button',
standalone: true,
template: `
<button class="btn" [class]="variant()">
<ng-content />
</button>
`,
styles: [`
.btn { padding: 8px 16px; border: none; cursor: pointer; border-radius: 4px; }
.primary { background: #007bff; color: white; }
.secondary { background: #6c757d; color: white; }
`]
})
export class ButtonComponent {
variant = input<'primary' | 'secondary'>('primary');
}

Make sure it is exported from libs/ui/src/index.ts:

export * from './lib/button/button.component';

Step 6: Use the Library in the Shop App​

In apps/shop/src/app/app.component.ts, import using the alias you found in tsconfig.base.json:

import { ButtonComponent } from '@my-workspace/ui'; // replace prefix if yours differs

@Component({
selector: 'app-root',
standalone: true,
imports: [ButtonComponent],
template: `
<h1>Shop</h1>
<lib-button variant="primary">Add to cart</lib-button>
<lib-button variant="secondary">View details</lib-button>
`
})
export class AppComponent {}

nx serve shop β€” the shared button renders inside the shop app.

Step 7: Visualize the Dependency Graph​

nx graph

The browser opens an interactive graph. Verify:

  • shop depends on ui
  • admin has no dependencies yet

Step 8: Run Affected Commands​

Make a change to ButtonComponent, then:

# Only build what changed + what depends on it
nx affected --target=build

# Only test what changed + what depends on it
nx affected --target=test

NX determines that changing ui affects shop (which depends on it) β€” so both are rebuilt. admin is untouched.

This is the key benefit in CI: you only pay for what changed.