Lab 27: NX — Monorepo from Scratch
📖 Resources
Section titled “📖 Resources”🚀 Starter Code
Section titled “🚀 Starter Code”No starter — you will create a fresh NX workspace from scratch.
This lab covers the same goals as Lab 26 — shared libraries, path aliases, dependency graph, and affected — but starts from an empty workspace instead of the Angular preset. This approach gives you full control over what gets installed and generated.
📝 Instructions
Section titled “📝 Instructions”Step 1: Create an Empty Workspace
Section titled “Step 1: Create an Empty Workspace”npx create-nx-workspace@latest my-workspace --preset=appsWhen prompted, choose No for Nx Cloud.
--preset=apps creates a minimal workspace with no demo code, no framework plugins, no generated application — just the structure:
my-workspace/├── packages/ ← placeholder (npm workspaces root)├── nx.json├── tsconfig.base.json├── tsconfig.json└── package.jsonThe apps/ and libs/ directories do not exist yet — they are created when you run generators in later steps.
Compare this to the Angular preset (Lab 26), which pre-generates a shop app and installs @nx/angular for you. Here, nothing is assumed.
Step 2: Add Angular Support
Section titled “Step 2: Add Angular Support”The empty workspace has no framework support yet. First, navigate into the workspace and export a flag that suppresses a TypeScript compatibility warning:
cd my-workspaceexport NX_IGNORE_UNSUPPORTED_TS_SETUP=trueNow add the Angular plugin:
npx nx add @nx/angularThis installs @nx/angular and registers it in nx.json. It does not generate any application — that is your next step.
Step 3: Generate the First Application
Section titled “Step 3: Generate the First Application”nx g @nx/angular:app shopWhen prompted, choose your stylesheet format (CSS or SCSS).
In Nx 23, apps are generated at the workspace root (not inside an apps/ subfolder):
shop/shop-e2e/Serve it:
nx serve shopOpen http://localhost:4200 — a blank Angular application with no demo content.
Step 4: Generate a Second Application
Section titled “Step 4: Generate a Second Application”nx g @nx/angular:app adminNow you have two independent apps in the same repository:
shop/shop-e2e/admin/admin-e2e/Run each independently:
nx serve shop # http://localhost:4200nx serve admin # http://localhost:4201Step 5: Generate a Shared UI Library
Section titled “Step 5: Generate a Shared UI Library”nx g @nx/angular:lib --name=ui --directory=libs/uiNote (Nx v23+): Positional arguments are no longer supported by this generator — always use
--name=uiexplicitly. Using--directory=libs/uiplaces the library atlibs/ui/in the workspace root.
The library is created at libs/ui/ with its own project.json. The generator also adds a scaffolded component inside libs/ui/src/lib/ui/ — you can leave it or delete it; it is not used in this lab.
Why libs/ and not 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. 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 used by any app |
libs/shop/shared-ui/ |
Shop scope only | Components specific to the shop domain |
Check the path alias Nx generated
After the generator runs, open tsconfig.base.json:
{ "compilerOptions": { "paths": { "@org/ui": ["./libs/ui/src/index.ts"] } }}TypeScript resolves @org/ui to libs/ui/src/index.ts. No webpack alias needed.
⚠️ The prefix depends on the npm org set when the workspace was created. Always read
tsconfig.base.json → compilerOptions.pathsto find your exact alias before using it in imports.
Tags and module boundaries
Open libs/ui/project.json and add tags:
{ "tags": ["scope:shared", "type:ui"]}Tags are enforced by @nx/enforce-module-boundaries in eslint.config.mjs. Without scope:shared, ESLint will block apps from importing this library once constraints are configured:
"@nx/enforce-module-boundaries": [ "error", { "depConstraints": [ { "sourceTag": "scope:shop", "onlyDependOnLibsWithTags": ["scope:shop", "scope:shared"] }, { "sourceTag": "scope:admin", "onlyDependOnLibsWithTags": ["scope:admin", "scope:shared"] } ] }]Step 6: Create a Shared Component
Section titled “Step 6: Create a Shared Component”nx g @nx/angular:component libs/ui/src/lib/button/button --exportNote (Nx v23+): Pass the full path — the
--projectflag has been removed. The--exportflag automatically adds the export tolibs/ui/src/index.ts.
In Angular 20, generated components use the short naming convention: the file is button.ts (not button.component.ts) and the class is Button (not ButtonComponent). standalone is the default and does not need to be declared.
Replace the contents of libs/ui/src/lib/button/button.ts with:
import { Component, input } from '@angular/core';
@Component({ selector: 'lib-button', 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 Button { variant = input<'primary' | 'secondary'>('primary');}The --export flag already added the export to libs/ui/src/index.ts. Verify it points to the right file:
export * from './lib/button/button';Step 7: Use the Library in the Shop App
Section titled “Step 7: Use the Library in the Shop App”In shop/src/app/app.component.ts:
import { Button } from '@org/ui'; // check tsconfig.base.json for your exact prefix
@Component({ selector: 'app-root', imports: [Button], 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 shopThe shared button renders inside the shop app. The admin app can import the same component from the same path — one source of truth.
Step 8: Visualize the Dependency Graph
Section titled “Step 8: Visualize the Dependency Graph”nx graphVerify:
shopdepends onuiadminhas no dependencies yet
Step 9: Run Affected Commands
Section titled “Step 9: Run Affected Commands”Make a change to Button, then:
nx affected --target=buildnx affected --target=testNx determines that changing ui affects shop — both are rebuilt. admin is untouched. In CI you only rebuild and retest what actually changed.
🔄 Preset Comparison
Section titled “🔄 Preset Comparison”| Lab 26 (Angular preset) | Lab 27 (apps preset) | |
|---|---|---|
| Setup | Interactive prompts | --preset=apps flag |
| Angular plugin | Pre-installed | npx nx add @nx/angular |
| First app | Auto-generated (shop) |
Manually generated |
| App location | shop/ at workspace root |
shop/ at workspace root |
| Demo code | Included | None |
| Best for | Quick Angular-only start | Multi-framework or controlled setup |

