Table of Contents


Nx Workspace Angular Step by Step Guide

Managing large-scale Angular applications can quickly become overwhelming without the right tools and structure. That’s where Nx Workspace comes in — a powerful monorepo tool that simplifies development, boosts performance, and improves collaboration. Whether you're building a modular enterprise app or managing multiple Angular projects, Nx provides a clean and scalable solution.

In this step-by-step guide, we’ll walk you through setting up an Nx Workspace with Angular from scratch. From creating your first workspace to understanding libraries, generators, and dependency graphs, this guide is designed for both beginners and experienced Angular developers looking to enhance their productivity and maintainability.

Prerequisites

To follow along, ensure you have:

Node.js V20.1.0 (should be greater than 16.9.0)

Angular CLI v19.2.10 (or later)

Once Node.js is installed, you can proceed to install Angular CLI.

Let’s start working on Project Setup Requirements

Step 1: Install the Nx CLI

To install the nx CLI globally using npm, open a VS Code terminal/console window. If you're using Windows, macOS, or Linux, the following command works the same way:


npm install -g nx to install nx globally						
						

This process might take a few moments as the necessary packages are downloaded and installed.

Step 2: Create a new Workspace

To create a workspace for angular using npx, use this command. If you are on a Mac or Linux type following command.


npx create-nx-workspace@latest [workspace-name] --preset=angular --strict						
						

Create a new folder for your project (e.g., angular_monorepo) and navigate into it. Open a terminal and execute the following command to scaffold a new nx workspace. The flag strict is for strict type checking. The name of the workspace is TestAngularMonoRepo.


npx create-nx-workspace@latest TestAngularMonoRepo--preset=angular --strict						
						

Answer the following prompts on creating workspace

• Choose an integrated repo for multiple projects or standalone projects? Select integrated monorepo

• Add application name - app-one

• Choose bundler -Es-build

• Choose stylesheet format - css

• Choose to enable server side Rendering - No

• Choose unit test runner - Jest

• Choose test runner to use end to end tests E2E - Playwright

• Choose CI provider - Github actions

After package installation add "strict": true in the compilerOptions tsconfig.base.json file at the root of your workspace. For more granular control, enable specific strict checks individually (e.g., "strictNullChecks": true) in tsconfig.json of project app-one angularCompilerOptions.

Step 3: Install plugins

Navigate into TestAngularMonoRepo by using command


cd TestAngularMonoRepo					
						

After this run the command to install plugins


Run ‘npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser’
Run ‘npm install --save-dev @nx/angular @nx/linter @nx/storybook’				
						

Step 4: QA for Project Setup Requirements

Run these commands in console window in project TestAngularMonoRepo

To serve the app run ‘npx nx serve app-name’


Run ‘nx serve app-one’
						

To Lint the app run ‘nx lint app-name’ and for libraries run command ‘nx lint lib-name’


Run ‘nx lint app-one’
						

To Build the app run ‘nx build app-name’ and for libraries run command ‘nx build lib-name’ and for production build add --configuration=production after the command


Run ‘nx build app-one’
						

To Test the app run ‘nx test app-name’ and for libraries run command ‘nx test lib-name‘


Run ‘nx test app-one’
						

For strict type checking check required flags strict:true are present in compilerOptions in tsconfig.json of app-one

Step 5: Creating New Application

To create a new angular application by using command nx g @nx/angular:application apps/application-name


Run ‘nx g @nx/angular:application apps/app-two’
						

Answer the prompts in console window

• Choose Bundler - esbuild

• Choose server side rendering - No

It will serve on another port if the first project is already serving.

Step 6: Creating Libraries

To create a library in feature, shared and ui directory, run command ‘nx g @nx/angular:library libs/directory-folder/lib-name --standalone --buildable’. Buildable flag is used to create the build of libraries.

At first create auth and dashboard libraries in feature directory


Run ‘nx g @nx/angular:library libs/feature/auth --standalone --buildable’
Run ‘nx g @nx/angular:library libs/feature/dashboard --standalone --buildable’
						

Create models, services, utils and constants library in shared directory


Run ‘nx g @nx/angular:library libs/shared/models --standalone --buildable’
Run ‘nx g @nx/angular:library libs/shared/services --standalone --buildable’
Run ‘nx g @nx/angular:library libs/shared/utils --standalone --buildable’
Run ‘nx g @nx/angular:library libs/shared/constants --standalone --buildable’
						

Create button and form library in ui directory


Run ‘nx g @nx/angular:library libs/ui/button --standalone --buildable’
Run ‘nx g @nx/angular:library libs/ui/form --standalone --buildable’
						

For domain-driven folder structure create service in auth library with command


Run ‘nx g @nx/angular:service libs/feature/auth/data-access --project=auth’
						

Step 7: QA for Application and Library Structure

To check each library build independently run command ‘nx build <lib-name>’ for each library.


Run ‘nx build auth’
Run ‘nx build dashboard’
Run ‘nx build models’
Run ‘nx build services’
Run ‘nx build utils’
Run ‘nx build constants’
Run ‘nx build button’
Run ‘nx build form’
						

To check for paths object for libraries in tsconfig.base.json for all libraries


"paths": {
  "@angular-monorepo/auth": ["libs/feature/auth/src/index.ts"],
  "@angular-monorepo/button": ["libs/ui/button/src/index.ts"],
  "@angular-monorepo/constants": ["libs/shared/constants/src/index.ts"],
  "@angular-monorepo/dashboard": ["libs/feature/dashboard/src/index.ts"],
  "@angular-monorepo/form": ["libs/ui/form/src/index.ts"],
  "@angular-monorepo/models": ["libs/shared/models/src/index.ts"],
  "@angular-monorepo/services": ["libs/shared/services/src/index.ts"],
  "@angular-monorepo/utils": ["libs/shared/utils/src/index.ts"]
},							
							

To check if library is Tree Shakable


import { Injectable } from '@angular/core';
@Injectable({
  providedIn: 'root',
})
export class DataAccessService {
  constructor() {}
}							
							

Step 8: Create Component

To create a new component in project app-one run this command


Run ‘nx g @nx/angular:component apps/app-one/src/app/login-page’
						

Step 9: Define Architectural boundaries

To define architectural boundaries in nx.json add an object named workspaceLayout and define keys for apps directory and library directory.


{
  "workspaceLayout": {
    "appsDir": "apps",
    "libsDir": "libs"
  },
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "defaultBase": "main",							
							

Define tags for libraries in nx.json at last of code


"tags": [
    "type:ui",
    "type:feature",
    "type:shared",
    "domain:auth",
    "domain:dashboard"
],
							

Add tags key in project.json of libraries

Add in project.json of form and button library


"tags": ["type:ui"],							
							

Add in project.json of models, utils, constants, services library


"tags": ["type:shared"],							
							

Add in project.json in auth library


"tags": ["type:feature", "domain:auth"],							
							

Add in project.json in dashboard library


"tags": ["type:feature", "domain:dashboard"],						
							

Add in project.json of app-one component


"tags": ["app:one"],						
							

Add in project.json of app-two component


"tags": ["app:two"],							
							

Step 10: Enforce Module boundaries

To enforce module boundaries do the changes in nx.json as mentioned below

Add targetDependencies object


"targetDependencies": {
    "build": [
      {
        "target": "build",
        "projects": "dependencies"
      }
    ]
  },				
							

Add dependencyConstraints object


"dependencyConstraints": [
    {
      "sourceTag": "domain:auth",
      "onlyDependsOnLibsWithTags": ["domain:auth", "type:shared"]
    },
    {
      "sourceTag": "domain:dashboard",
      "onlyDependsOnLibsWithTags": ["domain:dashboard", "type:shared"]
    },
    {
      "sourceTag": "type:feature",
      "onlyDependsOnLibsWithTags": ["type:feature", "type:shared"]
   },
	{
      "sourceTag": "type:ui",
      "onlyDependsOnLibsWithTags": ["type:ui", "type:shared"]
    },
    {
      "sourceTag": "*",
      "notDependsOnLibsWithTags": ["domain:auth", "domain:dashboard"],
      "onlyDependsOnLibsWithTags": [
        "*",
        "!domain:auth",
        "!domain:dashboard",
        "type:shared"
      ]
    }
  ]
							

Step 11: Enable Linting Rules

Add linting rules to prevent circular dependencies. First of all run the command


Run ‘npm install -D eslint-plugin-import’						
						

Secondly, in eslint.config.mjs of workspace add rules key for files ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx']


{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
  '@typescript-eslint/no-empty-function': 'warn',
  '@nx/enforce-module-boundaries': [
	'error',
	{
	  enforceBuildableLibDependency: true,
	  allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
	  depConstraints: [
		{
		  sourceTag: 'domain:auth',
		  onlyDependOnLibsWithTags: ['domain:auth', 'type:shared'],
		},
		{
		  sourceTag: 'domain:dashboard',
		  onlyDependOnLibsWithTags: ['domain:dashboard', 'type:shared'],
		},
		{
		  sourceTag: 'type:feature',
		  onlyDependOnLibsWithTags: ['type:feature', 'type:shared'],
		},
		{
		  sourceTag: 'type:ui',
		  onlyDependOnLibsWithTags: ['type:ui', 'type:shared'],
		},
		{
		  sourceTag: '*',
		  notDependOnLibsWithTags: ['domain:auth', 'domain:dashboard'],
		  onlyDependOnLibsWithTags: [
			'*',
			'!domain:auth',
			'!domain:dashboard',
			'type:shared',
		  ],
		},
	  ],
	},
  ],
},
},
						

Add rules in eslint.config.mjs of app-one component


{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
  '@nx/enforce-module-boundaries': [
	'error',
	{
	  enforceBuildableLibDependency: true,
	  allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
	  depConstraints: [
		// ... other existing constraints
		{
		  sourceTag: ['app:one'],
		  onlyDependOnLibsWithTags: [
			'app:one',
			'domain:auth',
			'domain:dashboard',
			'type:shared',
		  ],
		},
		// ... any other constraints
	  ],
	},
  ],
  // ... other rules
},
},
						

Step 12: Application and Library Structure

To Visualize with Dependency Graph boundaries run


Run ‘nx dep-graph’						
						

Step 13: Serve App

To serve an angular app run command ‘nx serve app-name’


Run ‘nx serve app-one’ or ‘nx s app-two’	
						

Step 14: Build App or library

To build an angular app run command ‘nx build app-name’


Run ‘nx build app-one’
Run ‘nx build app-two’	
						

To build an library run ‘nx build library-name’


Run ‘nx build auth’
Run ‘nx build dashboard’
Run ‘nx build form’
Run ‘nx build button’
Run ‘nx build constants’
Run ‘nx build utils’
Run ‘nx build models’
Run ‘nx build services’
						

Step 15: Test App or Library

To test an Angular app run ‘nx test app-name’


Run ‘nx test app-one’
Run ‘nx test app-two’						
						

To test an angular library run ‘nx test library-name’


Run ‘nx test auth’
Run ‘nx test dashboard
Run ‘nx test form’
Run ‘nx test button’
Run ‘nx test constants’
Run ‘nx test utils’
Run ‘nx test models’
Run ‘nx test services’						
						

Step 16: Lint App or Library

To lint an Angular app run ‘nx lint app-name’


Run ‘nx lint app-one’
Run ‘nx lint app-two’						
						

To lint a library run ‘nx lint library-name’


Run nx lint lib-name
Run nx lint auth
Run nx lint dashboard
Run nx lint form
Run nx lint button
Run nx lint constants
Run nx lint utils
Run nx lint models
Run nx lint services
						

Step 17: Format Checking

For consistent formatting run this command to check formatting


Run ‘nx format:check’
						

And to write formatting


Run ‘nx format:write’
						

Step 18: Storybook Setup for UI Libraries

The steps for setup of storybooks are mentioned below

To Integrate Storybook for @libs/ui-* libraries run command ‘nx g @nx/angular:storybook-configuration <your-ui-lib-name>'


Run ‘nx g @nx/angular:storybook-configuration button’
								

Answer the following questions to set up storybook

• Do you want to set up Storybook interaction tests? (Y/n) · true

• Automatically generate *.stories.ts files for components declared in this project? (Y/n) · true

• Configure a static file server for the storybook instance? (Y/n) · true

Change the component Selector in button.component.ts to '[lib-button]' to change the selector to attribute


import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';


@Component({
  selector: '[lib-button]',
  imports: [CommonModule],
  templateUrl: './button.component.html',
  styleUrl: './button.component.css',							
							

If you face linting error in es.lint.mjs in button library then change the value of type attribute to [‘element’, ‘attribute’] inside @angular-eslint/component-selector


  {
    files: ['**/*.ts'],
    rules: {
      '@angular-eslint/directive-selector': [
        'error',
        {
          type: 'attribute',
          prefix: 'lib',
          style: 'camelCase',
        },
      ],
      '@angular-eslint/component-selector': [
        'error',
        {
          type: '["element", "attribute"] ',
          prefix: 'lib',
          style: 'kebab-case',
        },
      ],
    },
  },						
							

Next step to add this data in button.component.stories.ts to create 4 types of styling


import {moduleMetadata, Meta, StoryFn } from '@storybook/angular';
import { ButtonComponent } from './button.component';
export default {
  title: 'ui-button',
  component: ButtonComponent,
  decorators:[
    moduleMetadata({
      imports:[],
    }),
  ],
} as Meta<ButtonComponent>;

const Template : StoryFn<ButtonComponent> = (args:ButtonComponent)=>({
  template:'<button lib-button [type]="type">Button Text</button>',
  props : args
})
export const Basic = Template.bind({})
  Basic.args= {
    type:'basic'
  }
export const Stroked = Template.bind({})
  Stroked.args= {
    type:'stroked'
  }
export const Flat = Template.bind({})
  Flat.args= {
    type:'flat'
  }
export const Raised = Template.bind({})
  Raised.args= {
    type:'raised'
  }

							
							

Add selector <ng-content></ng-content> to button.component.html


<ng-content></ng-content>					
							

Add code to button.component.css to define different designs for basic, stroked, raised and flat.


:host{
    height: 40px;
    padding: 0 24px;
    font-size: 1rem;
    border-radius: 5px;
    box-shadow: none;
    color: black;
    &.rm-basic{
        background: none;
        border: none;
    }
    &.rm-stroked{
        border: 1px solid grey;
        background: none;
    }
    &.rm-raised{
        border: none;
        background: white;
        box-shadow: 1px 2px 5px 1px grey;
    }
    &.rm-flat{
        border: none;
        background: rgb(235, 230, 230);
    }
}
				
							

Add the code to button.component.ts


import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';


@Component({
  selector: '[lib-button]',
  imports: [CommonModule],
  templateUrl: './button.component.html',
  styleUrl: './button.component.css',
  changeDetection:ChangeDetectionStrategy.OnPush,
  host:{
    '[class.rm-flat]':'type==="flat"',
    '[class.rm-raised]':'type==="raised"',
    '[class.rm-stroked]':'type==="stroked"',
    '[class.rm-basic]':'type==="basic"'
  }
})
export class ButtonComponent{
  @Input() type : 'flat' | 'basic' | 'raised' | 'stroked' = 'raised'
  constructor(){}
}
							
							

Step 19: Configure CI for storybook

Configure CI to validate Storybook builds add this to .github/workflows/ci.yml after main job


storybook:
    runs-on: ubuntu-latest
    needs: main
    strategy:
      matrix:
        ui_lib: ['button']
    steps:
      - uses: actions/checkout@v4
        with:
          filter: tree:0
          fetch-depth: 0


      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'


      - run: npm ci --legacy-peer-deps


      - name: Build Storybook for ${{ matrix.ui_lib }}
        run: npx nx build-storybook ${{ matrix.ui_lib }}
						
						

Step 20: QA for Application and Library Structure

To verify components display of story book run the following command


Run ‘nx run button:storybook’							
							

To confirm that there is no storybook build errors run the this command


Run ‘nx run button:build-storybook’						
							

Step 21: CI/CD Readiness

Provided a sample CI configuration using github actions that runs build, test, lint, and format checks. Runs Storybook build (optional).


name: CI


on:
  push:
    branches:
      - main
  pull_request:


permissions:
  actions: read
  contents: read


jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          filter: tree:0
          fetch-depth: 0


      # This enables task distribution via Nx Cloud
      # Run this command as early as possible, before dependencies are installed
      # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
      # Uncomment this line to enable task distribution
      # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci"


      # Cache node_modules
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'


      - run: npm ci --legacy-peer-deps
      - run: npx playwright install --with-deps
      - uses: nrwl/nx-set-shas@v4


      # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
      # - run: npx nx-cloud record -- echo Hello World
      # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
      # When you enable task distribution, run the e2e-ci task instead of e2e
      - run: npx nx affected -t lint test build e2e --base=main --head=HEAD
 
  storybook:
    runs-on: ubuntu-latest
    needs: main
    strategy:
      matrix:
        ui_lib: ['button']
    steps:
      - uses: actions/checkout@v4
        with:
          filter: tree:0
          fetch-depth: 0


      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'


      - run: npm ci --legacy-peer-deps


      - name: Build Storybook for ${{ matrix.ui_lib }}
        run: npx nx build-storybook ${{ matrix.ui_lib }}
						
						

Step 22: QA for CI/CD Readiness

Push to the Git repo and trigger CI pipeline and confirm pipeline runs all configured steps successfully.

Conclusion :

Nx is an open source build system that provides tools and techniques to manage monorepo. Monorepo is a repository that contains multiple different projects. It is typescript based monorepo tool, primarily supporting Node Js based frontend, backend apps, and libraries with a single code base. It provides linting, formatting, testing and builds.

Ready to Build Something Amazing?

Get in touch with Prishusoft – your trusted partner for custom software development. Whether you need a powerful web application or a sleek mobile app, our expert team is here to turn your ideas into reality.

image