Lab 26: NX — Monorepo Architecture
📖 Resources
Section titled “📖 Resources”🚀 Starter Code
Section titled “🚀 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
Section titled “📝 Instructions”Step 1: Create the NX Workspace
Section titled “Step 1: Create the NX Workspace”npx create-nx-workspace@latest my-workspaceWhen 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.jsonStep 2: Serve the App
Section titled “Step 2: Serve the App”cd my-workspacenx serve shopOpen http://localhost:4200 — the default Angular app is running.
Step 3: Generate a Second Application
Section titled “Step 3: Generate a Second Application”nx g @nx/angular:app adminNow you have two independent apps sharing the same repository:
apps/├── shop/└── admin/Run each independently:
nx serve shop # http://localhost:4200nx serve admin # http://localhost:4201Step 4: Generate a Shared UI Library
Section titled “Step 4: Generate a Shared UI Library”nx g @nx/angular:lib ui --directory=libs/uiNote (Nx v22+): Pass
uias the positional name and--directory=libs/uias the full output path. Avoid combining--name=uiand--directory=libs/uitogether — Nx treats--directoryas the complete path, so the two flags can produce a nestedlibs/ui/ui/instead of the intendedlibs/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
| Path | Scope | Use when… |
|---|---|---|
libs/ui/ |
Workspace-wide | Generic UI primitives (buttons, icons, typography) used by any app |
libs/shop/shared-ui/ |
Shop scope only | Components 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 choseacme, the alias will be@acme/ui. Always readtsconfig.base.json → compilerOptions.pathsto 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
Section titled “Step 5: Create a Shared Component”Generate a button component inside the library:
nx g @nx/angular:component libs/ui/src/lib/button/button --exportNote (Nx v22+): The
--projectflag 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
Section titled “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
Section titled “Step 7: Visualize the Dependency Graph”nx graphThe browser opens an interactive graph. Verify:
shopdepends onuiadminhas no dependencies yet
Step 8: Run Affected Commands
Section titled “Step 8: Run Affected Commands”Make a change to ButtonComponent, then:
# Only build what changed + what depends on itnx affected --target=build
# Only test what changed + what depends on itnx affected --target=testNX 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.

