Skip to main content

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.

Why start from scratch?

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
Why this flag?

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=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

PathScopeUse when…
libs/ui/Workspace-wideGeneric UI primitives used by any app
libs/shop/shared-ui/Shop scope onlyComponents 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"] }
]
}
]

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 --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';

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:

  • shop depends on ui
  • admin has 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)
SetupInteractive prompts--preset=apps flag
Angular pluginPre-installednpx nx add @nx/angular
First appAuto-generated (shop)Manually generated
App locationshop/ at workspace rootshop/ at workspace root
Demo codeIncludedNone
Best forQuick Angular-only startMulti-framework or controlled setup