Lab 27: NX β Monorepo from Scratch
π Resourcesβ
π 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β
Step 1: Create an Empty Workspaceβ
npx create-nx-workspace@latest my-workspace --preset=apps
When 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.json
The 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.
The --preset=apps approach is useful when:
- You want to mix frameworks (Angular + React in the same monorepo)
- You want to audit exactly what is installed
- You are setting up a monorepo and will add frameworks incrementally
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-workspace
export NX_IGNORE_UNSUPPORTED_TS_SETUP=true
Nx 23 creates workspaces with TypeScript project references enabled by default. Angular does not support project references (see angular#37276). Setting this variable lets all Angular generators proceed without blocking. Keep it active for the entire lab session.
Now add the Angular plugin:
npx nx add @nx/angular
This 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β
nx g @nx/angular:app shop
When 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 shop
Open http://localhost:4200 β a blank Angular application with no demo content.
Step 4: Generate a Second Applicationβ
nx g @nx/angular:app admin
Now you have two independent apps in the same repository:
shop/
shop-e2e/
admin/
admin-e2e/
Run each independently:
nx serve shop # http://localhost:4200
nx serve admin # http://localhost:4201
Step 5: Generate a Shared UI Libraryβ
nx g @nx/angular:lib --name=ui --directory=libs/ui
Note (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β
nx g @nx/angular:component libs/ui/src/lib/button/button --export
Note (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β
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 shop
The 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β
nx graph
Verify:
shopdepends onuiadminhas no dependencies yet
Step 9: Run Affected Commandsβ
Make a change to Button, then:
nx affected --target=build
nx affected --target=test
Nx determines that changing ui affects shop β both are rebuilt. admin is untouched. In CI you only rebuild and retest what actually changed.
π 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 |