Skip to content

Lab 27: NX — Monorepo from Scratch

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.

Terminal window
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 empty workspace has no framework support yet. First, navigate into the workspace and export a flag that suppresses a TypeScript compatibility warning:

Terminal window
cd my-workspace
export NX_IGNORE_UNSUPPORTED_TS_SETUP=true

Now add the Angular plugin:

Terminal window
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.

Terminal window
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:

Terminal window
nx serve shop

Open http://localhost:4200 — a blank Angular application with no demo content.

Terminal window
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:

Terminal window
nx serve shop # http://localhost:4200
nx serve admin # http://localhost:4201
Terminal window
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=ui explicitly. Using --directory=libs/ui places the library at libs/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.paths to 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"] }
]
}
]
Terminal window
nx g @nx/angular:component libs/ui/src/lib/button/button --export

Note (Nx v23+): Pass the full path — the --project flag has been removed. The --export flag automatically adds the export to libs/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';

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 {}
Terminal window
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.

Terminal window
nx graph

Verify:

  • shop depends on ui
  • admin has no dependencies yet

Make a change to Button, then:

Terminal window
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.

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