Nx Workspace: A Hands-On Guide to Monorepos (Latest Version)

Nx Workspace: A Hands-On Guide to Monorepos (Latest Version)

Welcome to the ultimate “learn by doing” guide for Nx Workspace! You’re about to embark on a journey that will transform how you approach software development, especially for projects involving multiple applications and shared code. This guide is built on the principle that the best way to learn is by getting your hands dirty.

We will walk through every concept with concrete commands, code snippets, and expected outputs. You’ll set up your environment, generate projects, write shared code, and see the power of Nx in action, step by step.

Let’s dive in!


1. Introduction to Nx Workspace

What is Nx Workspace?

Nx (pronounced “en-ex” and short for “Nrwl Extensions”) is an open-source, extensible build system that provides first-class support for monorepos. In simple terms, Nx helps you manage multiple applications and libraries within a single Git repository efficiently. It understands your codebase’s structure and dependencies, optimizing common development tasks like building, testing, and linting.

Imagine a single folder (your monorepo) containing your entire project: a React frontend, an Angular admin panel, a Node.js API, and shared UI components. Nx provides the tools to make this complex setup easy to manage and incredibly fast.

Why learn Nx Workspace? (Benefits, Use Cases, Industry Relevance)

Monorepos, when managed well, offer immense advantages. Nx amplifies these benefits and solves many challenges.

Benefits of using Nx in a Monorepo:

  • ⚡️ Faster Development Cycles: Nx’s intelligent caching and affected commands (which we’ll explore hands-on) mean you only build, test, and lint what’s necessary, saving significant time.
  • 🤝 Effortless Code Sharing: Share UI components, utility functions, data models, or backend logic across different applications without complicated publishing steps. This promotes consistency and reduces duplication.
  • ✅ Consistent Tooling & Standards: Nx’s code generators ensure all new projects are set up with a standardized toolchain (ESLint, Jest, Cypress) and best practices, reducing configuration overhead.
  • 🧐 Clear Dependency Management: Nx builds a visual dependency graph of your projects, making it easy to understand relationships and enforce architectural boundaries.
  • 🚀 Scalability: Designed for projects of all sizes, from small teams to large enterprises, handling hundreds of projects and developers efficiently.
  • ✨ Enhanced Developer Experience: Features like code generation, integrated VS Code extensions, and smart task execution boost productivity and make large codebases navigable.

Common Use Cases:

  • Full-Stack Development: A single monorepo for your React/Angular/Vue frontend and your Node.js/Spring Boot backend.
  • Shared Component Libraries: Building a design system that feeds components to multiple applications.
  • Micro-Frontends/Microservices: Managing independently deployable services or UI fragments within a unified repository.
  • Multi-Platform Apps: Web, mobile (React Native/Expo), and desktop apps sharing core logic.

Setting up your development environment

Let’s get your machine ready for Nx.

Prerequisites:

  1. Node.js (v20.19.0 or later): Nx relies on Node.js.

    • Check your version: Open your terminal and run:
      node -v
      
      Expected Output (or similar):
      v20.19.0
      
    • If you need to install/update: Visit nodejs.org or use nvm (Node Version Manager) for easier version switching.
  2. npm (Node Package Manager): Comes bundled with Node.js.

    • Check your version:
      npm -v
      
      Expected Output (or similar):
      10.5.2
      

    (Optional: You can also use Yarn or pnpm as your package manager. For this guide, we’ll stick with npm commands.)

Installation and Workspace Creation (Nx v21.4.x - Latest Stable):

This is the first hands-on step! We’ll create a new Nx workspace named my-nx-org.

  1. Open your terminal or command prompt.

  2. Run the Nx workspace creation command:

    npx create-nx-workspace@latest my-nx-org
    
    • npx: Executes a Node.js package directly without global installation. This ensures you always use the latest create-nx-workspace utility.
    • create-nx-workspace@latest: The command to initialize an Nx workspace.
    • my-nx-org: The name of the directory Nx will create for your workspace.
  3. Follow the interactive prompts: You’ll be asked a series of questions. For this beginner guide, we’ll choose options that give us a flexible starting point.

    ✔ Where would you like to create your workspace? · my-nx-org
    ✔ Which stack would you like to use? · None
    ✔ Would you like to use Prettier for code formatting? (Y/n) · Yes
    ✔ Which CI provider would you like to use? · Do it later
    ✔ Would you like remote caching to make your build faster? (Y/n) · No
    
    • Which stack would you like to use? · None: This is crucial. Selecting None gives us an empty workspace where we can add any type of projects (React, Angular, Node.js, etc.) later.
    • The other options are generally good defaults for learning.

    Expected Output (after prompts, installation will take some time):

    NX Creating workspace my-nx-org.
    
    NX The workspace has been created successfully.
    You can now navigate to the folder and explore your workspace.
    
    cd my-nx-org
    npm start # Starts the Nx Console if you have VSCode or some editor installed
    
  4. Navigate into your new workspace:

    cd my-nx-org
    

Congratulations! You’ve just created your first Nx Workspace.

Exercise 1.1: Verify your workspace

  • List the contents of your new directory:
    ls -F
    
    Expected Output (or similar):
    ./        .vscode/  apps/     nx.json         package-lock.json  package.json  README.md  tsconfig.base.json
    ../       .gitignore  libs/     node_modules/   pnpm-lock.yaml     projects/     tools/     yarn.lock
    
    • You might see package-lock.json, pnpm-lock.yaml, or yarn.lock depending on your npm configuration.
  • Open the workspace in your code editor (e.g., VS Code):
    code .
    
    Take a moment to look at the files. Don’t worry if it doesn’t all make sense yet; we’ll break it down.

2. Core Concepts and Fundamentals

Let’s dissect the structure and core ideas that make Nx tick.

Understanding the Workspace Structure

Your my-nx-org directory isn’t just a random collection of files. It’s an organized home for all your projects.

my-nx-org/
├── .vscode/             # Editor-specific settings (e.g., recommended extensions for VS Code)
├── apps/                # 📂 Your applications live here (frontends, backends, CLIs)
├── libs/                # 📂 Your shared libraries live here (UI components, utilities, data access)
├── .gitignore           # Specifies files/folders to ignore in Git
├── nx.json              # ⚙️ Nx workspace configuration - defines how Nx behaves
├── package.json         # 📦 Root package.json - workspace-level dependencies and scripts
├── package-lock.json    # 🔒 Dependency lock file (or pnpm-lock.yaml / yarn.lock)
├── README.md            # Basic workspace README
└── tsconfig.base.json   # 📄 Base TypeScript configuration for all TS projects in the workspace
  • apps/: This is where your runnable applications will reside. Think of them as independent, deployable units. Examples: a React website, an Angular admin panel, a Node.js API server.
  • libs/: This directory is for your shared, reusable code organized into libraries. Libraries are not directly runnable; they are consumed by applications or other libraries. This is the cornerstone of code sharing in a monorepo.
  • nx.json: This file is the central configuration for your Nx workspace. It tells Nx about the global setup, how to run tasks, handle caching, and define rules for your projects.
  • package.json: This is the root package.json for your entire monorepo. It lists all the shared development and production dependencies. Nx cleverly uses your package manager’s workspace features (like npm workspaces) to manage dependencies efficiently.
  • tsconfig.base.json: Crucial for TypeScript projects. It defines base compiler options and, importantly, sets up path aliases (e.g., @my-nx-org/shared/ui) so your projects can import from libraries easily.

Key Nx Concepts

  1. Projects (Apps & Libraries): In Nx, every application and every library is a “project.” Each project has its own project.json file (or a configuration section in nx.json for simpler setups) that defines its type, source code location, and available commands (called “targets”).

    • Applications (Apps): Runnable, deployable codebases (e.g., admin-dashboard, my-api).
    • Libraries (Libs): Reusable modules of code, consumed by other projects (e.g., ui-components, data-models). They promote modularity, testability, and code reuse.
  2. Generators: Generators are powerful tools for scaffolding new code. Instead of manually creating files, folders, and configurations for a new application, library, or component, you use an Nx generator.

    • Why use Generators?

      • Consistency: Every new project starts with the same, pre-defined structure and tooling.
      • Speed: Quickly scaffold complex setups with a single command.
      • Best Practices: Generators often embed framework-specific best practices.
      • Automatic Configuration: They automatically update relevant configuration files (e.g., nx.json, tsconfig.base.json, project.json).
    • Syntax: nx generate <plugin>:<generator-name> <project-name> [options] or the shorthand nx g <plugin>:<generator-name> <project-name> [options].

      • <plugin>: An Nx plugin (e.g., @nx/react, @nx/angular, @nx/node). These provide generators and executors for specific technologies.
      • <generator-name>: The specific generator (e.g., app, lib, component).
      • <project-name>: The name of the new project/item.
      • [options]: Configuration flags (e.g., --style=scss, --routing).
  3. Executors (Targets/Builders): An executor defines a specific task or command that can be run against a project. These are defined within a project’s project.json file under the targets property.

    • Common Targets: build, serve, test, lint, e2e.

    • Why use Executors?

      • Unified Interface: A consistent way to run tasks across all projects, regardless of the underlying technology.
      • Optimization Hook: Nx wraps these executors with its caching and dependency analysis logic.
      • Configurability: Targets can have different configurations (e.g., serve:development vs. serve:production).
    • Syntax: nx run <project-name>:<target-name>[:<configuration>] [options] or the shorthand nx <target-name> <project-name> [options].

  4. Dependency Graph: Nx automatically analyzes your code’s import statements and project configurations to build a visual map of how your projects depend on each other.

    • Why is it important?

      • Visualization: Helps you understand your architecture at a glance.
      • Optimization (affected commands): This graph is the foundation for Nx’s intelligence. If you change a library, Nx knows exactly which applications and other libraries consume it and thus need to be rebuilt, retested, or relinted.
      • Architectural Enforcement: You can define rules to prevent unwanted dependencies (e.g., a UI component library shouldn’t import from a backend API client).
    • Command: nx graph

  5. Caching: One of Nx’s most powerful features for speed. Nx caches the outputs of tasks (builds, tests, linting). If you run a task, Nx computes a unique identifier (a hash) based on all its inputs (source code, dependencies, configuration, environment variables). If those inputs haven’t changed since the last run, Nx will instantly restore the outputs from its cache instead of re-running the task.

    • Local Caching: Stores cached results on your local machine.
    • Remote Caching (Nx Cloud): For teams, it shares cache hits across all developers and CI/CD pipelines, providing massive speedups. (We opted out of Nx Cloud for now, but it’s a key feature for teams).
    • Why is it awesome?
      • Blazing Fast CI: Drastically reduces CI pipeline times.
      • Faster Local Development: No more waiting for redundant builds or tests.
      • Consistent Builds: Ensures that the same inputs always produce the same outputs.

Exercises/Mini-Challenges:

Let’s make these concepts concrete.

Exercise 2.1: Explore the nx.json configuration

  1. Open nx.json in your my-nx-org workspace.

    code nx.json
    
  2. Look for the tasksRunnerOptions and targetDefaults sections. These define how tasks are run and default settings for targets like build, serve, etc.

    Expected Content (or similar, Nx updates frequently):

    // nx.json
    {
      "$schema": "./node_modules/nx/schemas/nx-schema.json",
      "npmScope": "my-nx-org", // Your workspace npm scope
      "affected": {
        "defaultBase": "main" // Default branch for comparing changes
      },
      "defaultProject": null, // Can set a default project for commands
      "namedInputs": {
        "default": ["{projectRoot}/**/*", "sharedGlobals"],
        "production": ["default"],
        "sharedGlobals": []
      },
      "generators": {}, // Custom generators can be configured here
      "targetDefaults": { // Default settings for targets across all projects
        "build": {
          "cache": true,
          "inputs": ["production", "^production"],
          "dependsOn": ["^build"]
        },
        "lint": {
          "cache": true,
          "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
          "dependsOn": ["^lint"]
        },
        "test": {
          "cache": true,
          "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
          "dependsOn": ["^test"]
        },
        "e2e": {
          "cache": true,
          "inputs": ["default", "^production"],
          "dependsOn": ["build"]
        }
      },
      "release": {
        "changelog": {
          "preset": "angular"
        }
      },
      "plugins": [] // Where installed plugins will be listed
    }
    
    • Notice npmScope: This is used for generating import paths for your libraries (e.g., @my-nx-org/my-lib).
    • targetDefaults: Here, cache: true is enabled by default for build, lint, and test. This is Nx’s caching in action! dependsOn specifies dependencies between targets (e.g., build might depend on lint).

Exercise 2.2: List installed plugins

  1. Run the nx list command to see which Nx plugins are currently installed in your workspace.
    nx list
    
    Expected Output (or similar):
    NX   Installed plugins:
    
    - @nx/js
    - @nx/linter
    - @nx/eslint
    - @nx/devkit
    - @nx/workspace
    - @nx/nx-cloud (if you connected)
    ... (other core Nx plugins)
    
    NX   Community plugins:
    
    No community plugins installed.
    
    • Initially, you’ll mostly see core Nx plugins (like @nx/js, @nx/linter, @nx/workspace). As we add React, Angular, and Node.js projects, their respective plugins will appear here.

Exercise 2.3: Generate your first React application

Let’s bring a real project into our empty workspace. We’ll use the @nx/react plugin. First, we need to install it.

  1. Install the @nx/react plugin:

    npm install -D @nx/react
    
    • -D (or --save-dev) installs it as a development dependency.

    Expected Output (or similar, showing packages installed):

    added 24 packages, and audited 25 packages in 2s
    ...
    npm WARN deprecated @types/prettier@2.7.3: This is a stub types package. ...
    
  2. Generate a new React application:

    nx g @nx/react:app my-react-app --style=css --routing=true
    
    • nx g: Shorthand for nx generate.
    • @nx/react:app: We’re using the app generator from the @nx/react plugin.
    • my-react-app: The name of our new application.
    • --style=css: Use plain CSS for styling.
    • --routing=true: Include React Router for navigation.

    Expected Output (after a series of file creations and modifications):

    NX Generating @nx/react:app
    
    CREATE apps/my-react-app/project.json
    CREATE apps/my-react-app/src/main.tsx
    ... (many more files created)
    UPDATE nx.json
    UPDATE tsconfig.base.json
    UPDATE package.json
    UPDATE nx.json
    NX Successfully ran generator @nx/react:app for my-react-app
    
    • Notice Nx tells you exactly what files were created and updated. This is a core benefit of generators!
  3. Explore the new React app:

    • Look in your apps/ directory. You should now see apps/my-react-app.
    • Open apps/my-react-app/project.json. This file contains the configurations and targets specific to my-react-app.

    Expected apps/my-react-app/project.json (partial):

    // apps/my-react-app/project.json
    {
      "name": "my-react-app",
      "$schema": "../../node_modules/nx/schemas/project-schema.json",
      "sourceRoot": "apps/my-react-app/src",
      "projectType": "application",
      "tags": [],
      "targets": {
        "build": {
          "executor": "@nx/react:webpack", // The executor for building
          // ... options for build
        },
        "serve": {
          "executor": "@nx/react:dev-server", // The executor for serving
          // ... options for serve
        },
        "lint": {
          "executor": "@nx/eslint:lint",
          // ... options for lint
        },
        "test": {
          "executor": "@nx/jest:jest",
          // ... options for test
        }
      }
    }
    
    • Here you see the projectType: "application" and various targets like build, serve, lint, and test, each with its specific executor.

Exercise 2.4: Serve your new React application

Let’s fire up your React app!

  1. Run the serve command:

    nx serve my-react-app
    
    • nx serve: Shorthand for nx run my-react-app:serve.
    • my-react-app: The name of the project you want to serve.

    Expected Output (server logs, will open a browser tab):

    NX Starting type checking for my-react-app
    NX Running @nx/react:dev-server for my-react-app
    
    > my-react-app@0.0.1 serve /Users/youruser/my-nx-org
    > nx serve my-react-app
    
    
    Browserslist: Failed to parse package.json from /Users/youruser/my-nx-org
    
    ... (Webpack compilation logs) ...
    
    NX Web Development Server is listening on http://localhost:4200
    NX Compiled successfully.
    
    • Your browser should automatically open to http://localhost:4200 (or a similar port). You’ll see the default Nx welcome page for your React app.
    • Press Ctrl+C in your terminal to stop the server.

Exercise 2.5: Visualize the dependency graph

This is where Nx starts to show its intelligence.

  1. Run the graph command:
    nx graph
    
    Expected Output (opens browser tab with graph):
    NX Opening the dependency graph in your browser...
    
    • A new browser tab will open, displaying an interactive graph. You should see a single node for my-react-app.
    • As we add more projects and dependencies, this graph will grow and become incredibly useful.

3. Intermediate Topics

Now that you’ve got the basics down, let’s expand our workspace with more projects and dive into the core reason for using Nx: code sharing with libraries.

3.1. Generating Different Project Types (Angular, Node.js)

We started with React. Let’s add an Angular frontend and a Node.js backend to make our monorepo truly multi-stack.

Exercise 3.1.1: Generate an Angular Application

  1. Install the @nx/angular plugin:

    npm install -D @nx/angular
    

    Expected Output (similar to React plugin install):

    added 12 packages, and audited 13 packages in 1s
    
  2. Generate a new Angular application: We’ll create a modern standalone Angular app.

    nx g @nx/angular:app my-angular-app --style=scss --routing --standalone
    
    • @nx/angular:app: The generator for Angular applications.
    • my-angular-app: Our new Angular project name.
    • --style=scss: Use SCSS for styling.
    • --routing: Set up Angular routing.
    • --standalone: Use Angular’s standalone component API, which is the current best practice.

    Expected Output (many files created, nx.json, tsconfig.base.json, package.json updated):

    NX Generating @nx/angular:app
    
    CREATE apps/my-angular-app/project.json
    CREATE apps/my-angular-app/src/main.ts
    ... (many more files)
    UPDATE nx.json
    UPDATE tsconfig.base.json
    UPDATE package.json
    UPDATE nx.json
    NX Successfully ran generator @nx/angular:app for my-angular-app
    
  3. Serve the Angular App:

    nx serve my-angular-app
    

    Expected Output (Angular CLI build process, opens browser):

    NX Starting type checking for my-angular-app
    NX Running @nx/angular:dev-server for my-angular-app
    
    
    ... (Angular compilation logs) ...
    
    ** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
    √ Compiled successfully.
    
    • Your browser should open http://localhost:4200. Since you likely already had my-react-app running (or it would use the next available port, e.g., 4201), make sure to check the exact URL in the output.
    • You’ll see the default Nx welcome page, but for your Angular app.
    • Press Ctrl+C to stop.

Exercise 3.1.2: Generate a Node.js (Express) API

Now for a backend!

  1. Install the @nx/node plugin:

    npm install -D @nx/node
    
  2. Generate a new Node.js Express application:

    nx g @nx/node:app my-express-api --framework=express
    
    • @nx/node:app: The generator for Node.js applications.
    • my-express-api: Our Node.js API project name.
    • --framework=express: We’re explicitly choosing the Express.js framework. Nx also supports nest for NestJS, or none for a bare Node.js app.

    Expected Output (files created, configurations updated):

    NX Generating @nx/node:app
    
    CREATE apps/my-express-api/project.json
    CREATE apps/my-express-api/src/main.ts
    ... (more files)
    UPDATE nx.json
    UPDATE tsconfig.base.json
    UPDATE package.json
    UPDATE nx.json
    NX Successfully ran generator @nx/node:app for my-express-api
    
  3. Serve the Node.js API:

    nx serve my-express-api
    

    Expected Output (server listening):

    NX Running @nx/node:node for my-express-api
    
    > my-nx-org-my-express-api@0.0.1 serve /Users/youruser/my-nx-org
    > node dist/apps/my-express-api/main.js
    
    Listening at http://localhost:3000/api
    
    • Open your browser or use a tool like Postman/Insomnia and navigate to http://localhost:3000/api.
    • You should see a simple JSON response: {"message":"Welcome to my-express-api!"}.
    • Press Ctrl+C to stop.

Exercise 3.1.3: Update the Dependency Graph

  1. Run nx graph again:
    nx graph
    
    • Observe how the graph has expanded! You now have three distinct application projects: my-react-app, my-angular-app, and my-express-api. They are currently independent.

3.2. Sharing Code with Libraries

This is where Nx’s monorepo power truly comes alive. We’ll create libraries to share UI components and data types.

Exercise 3.2.1: Generate a Shared UI Library (for React)

We’ll create a React-based UI library that my-react-app can consume.

  1. Generate a new React library:

    nx g @nx/react:lib ui-components --directory=shared/ui --buildable --publishable --importPath=@my-nx-org/shared/ui-components --tags="scope:shared,type:ui"
    
    • @nx/react:lib: The generator for React libraries.
    • ui-components: The library’s name.
    • --directory=shared/ui: This creates the library within libs/shared/ui, promoting a structured organization.
    • --buildable: This library can be built independently, which is important if you want to publish it or optimize build times.
    • --publishable: Sets up the necessary package.json for publishing to npm. (Requires --buildable).
    • --importPath=@my-nx-org/shared/ui-components: This is the custom TypeScript path alias. This allows you to import components like import { Button } from '@my-nx-org/shared/ui-components'; instead of long relative paths. Nx automatically configures tsconfig.base.json.
    • --tags="scope:shared,type:ui": We’re adding tags. These are crucial for module boundary enforcement later (see Section 4). scope:shared means it’s available to all, type:ui categorizes it.

    Expected Output (files created in libs/shared/ui/ui-components, updates to nx.json, tsconfig.base.json):

    NX Generating @nx/react:lib
    
    CREATE libs/shared/ui/ui-components/project.json
    CREATE libs/shared/ui/ui-components/src/index.ts
    ... (many more files, including test files)
    UPDATE nx.json
    UPDATE tsconfig.base.json
    UPDATE package.json
    NX Successfully ran generator @nx/react:lib for ui-components
    
  2. Add a simple Button component to the UI library:

    nx g @nx/react:component button --project=ui-components --export
    
    • @nx/react:component: Generator for React components.
    • button: Name of the component.
    • --project=ui-components: Specifies that this component belongs to our ui-components library.
    • --export: Exports the component from the library’s main entry file (index.ts).

    Expected Output (files created in libs/shared/ui/ui-components/src/lib/button):

    NX Generating @nx/react:component
    
    CREATE libs/shared/ui/ui-components/src/lib/button/button.spec.tsx
    CREATE libs/shared/ui/ui-components/src/lib/button/button.module.css
    CREATE libs/shared/ui/ui-components/src/lib/button/button.tsx
    UPDATE libs/shared/ui/ui-components/src/index.ts
    NX Successfully ran generator @nx/react:component for button
    
  3. Edit libs/shared/ui/ui-components/src/lib/button/button.tsx:

    // libs/shared/ui/ui-components/src/lib/button/button.tsx
    import styles from './button.module.css';
    
    /* eslint-disable-next-line */
    export interface ButtonProps {
      label: string;
      onClick: () => void;
      variant?: 'primary' | 'secondary';
    }
    
    export function Button(props: ButtonProps) {
      const { label, onClick, variant = 'primary' } = props;
      return (
        <button className={`${styles['container']} ${styles[variant]}`} onClick={onClick}>
          {label}
        </button>
      );
    }
    
    export default Button;
    
  4. Edit libs/shared/ui/ui-components/src/lib/button/button.module.css:

    /* libs/shared/ui/ui-components/src/lib/button/button.module.css */
    .container {
      padding: 10px 20px;
      border-radius: 5px;
      border: none;
      cursor: pointer;
      font-size: 16px;
      transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
      margin: 5px;
    }
    
    .container:hover {
      transform: translateY(-1px);
    }
    
    .primary {
      background-color: #007bff;
      color: white;
    }
    
    .primary:hover {
      background-color: #0056b3;
    }
    
    .secondary {
      background-color: #6c757d;
      color: white;
    }
    
    .secondary:hover {
      background-color: #5a6268;
    }
    

Exercise 3.2.2: Generate a Shared JavaScript/TypeScript Library (for Frontend & Backend Types)

This library will hold common TypeScript interfaces or utility functions that both your frontend applications and backend API might need.

  1. Generate a new JavaScript/TypeScript library:

    nx g @nx/js:lib data-models --directory=shared/data --importPath=@my-nx-org/shared/data-models --tags="scope:shared,type:data"
    
    • @nx/js:lib: The generator for generic JS/TS libraries.
    • data-models: Library name.
    • --directory=shared/data: Placed in libs/shared/data.
    • --importPath=@my-nx-org/shared/data-models: Custom import path.
    • --tags="scope:shared,type:data": Tags for module boundaries.

    Expected Output:

    NX Generating @nx/js:lib
    
    CREATE libs/shared/data/data-models/project.json
    CREATE libs/shared/data/data-models/src/index.ts
    ... (other files)
    UPDATE nx.json
    UPDATE tsconfig.base.json
    UPDATE package.json
    NX Successfully ran generator @nx/js:lib for data-models
    
  2. Define a simple Product interface and a utility function in libs/shared/data/data-models/src/lib/data-models.ts:

    // libs/shared/data/data-models/src/lib/data-models.ts
    
    export interface Product {
      id: string;
      name: string;
      price: number;
      description?: string;
      currency?: string; // Add currency
    }
    
    export interface User {
      id: string;
      username: string;
      email: string;
    }
    
    // Example utility function to format currency
    export function formatCurrency(amount: number, currency: string = 'USD'): string {
      return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: currency,
      }).format(amount);
    }
    
  3. Export the new definitions from libs/shared/data/data-models/src/index.ts:

    // libs/shared/data/data-models/src/index.ts
    export * from './lib/data-models';
    

3.3. Using Shared Libraries in Applications

Now, let’s see these libraries in action by consuming them in our applications.

Exercise 3.3.1: Use the UI Library in my-react-app

  1. Open apps/my-react-app/src/app/app.tsx and modify it:

    // apps/my-react-app/src/app/app.tsx
    import { useState } from 'react';
    import { Button } from '@my-nx-org/shared/ui-components'; // <--- Import our shared Button!
    import NxWelcome from './nx-welcome';
    
    import './app.module.css'; // Assuming you have some app-level CSS if needed
    
    export function App() {
      const [count, setCount] = useState(0);
    
      const handlePrimaryClick = () => {
        setCount((prev) => prev + 1);
        console.log('Primary button clicked!');
      };
    
      const handleSecondaryClick = () => {
        alert('Secondary button clicked!');
      };
    
      return (
        <div className="react-app-container">
          <NxWelcome title="my-react-app" />
    
          <main>
            <h1>Welcome to my-react-app!</h1>
            <p>Here are some shared buttons:</p>
            <Button label={`Primary Clicked ${count} times`} onClick={handlePrimaryClick} variant="primary" />
            <Button label="Secondary Action" onClick={handleSecondaryClick} variant="secondary" />
          </main>
        </div>
      );
    }
    
    export default App;
    
    • Crucially: Notice the clean import: import { Button } from '@my-nx-org/shared/ui-components';. This is thanks to Nx automatically configuring TypeScript path aliases.
  2. Serve my-react-app:

    nx serve my-react-app
    
    • Go to http://localhost:4200 (or whatever port Nx assigns).
    • You should now see the two custom buttons you created in the ui-components library, right inside your React application! Click them to see their effects.
    • Press Ctrl+C to stop.

Exercise 3.3.2: Use the Data Models Library in my-express-api

We’ll update our Node.js API to use the Product interface and formatCurrency function from our data-models library.

  1. Open apps/my-express-api/src/main.ts and modify it:

    // apps/my-express-api/src/main.ts
    import express from 'express';
    import { Product, formatCurrency } from '@my-nx-org/shared/data-models'; // <-- Import shared types and utility
    
    const app = express();
    app.use(express.json()); // Enable JSON body parsing
    
    // In a real app, these would come from a database
    const products: Product[] = [
      { id: 'p1', name: 'Laptop', price: 1200.00, description: 'Powerful portable computer', currency: 'USD' },
      { id: 'p2', name: 'Keyboard', price: 75.50, description: 'Mechanical RGB keyboard', currency: 'USD' },
      { id: 'p3', name: 'Mouse', price: 30.00, description: 'Ergonomic wireless mouse', currency: 'EUR' },
    ];
    
    app.get('/api/products', (req, res) => {
      // Use the shared formatCurrency function
      const productsWithFormattedPrice = products.map(p => ({
        ...p,
        formattedPrice: formatCurrency(p.price, p.currency)
      }));
      res.json(productsWithFormattedPrice);
    });
    
    app.get('/api/products/:id', (req, res) => {
      const product = products.find(p => p.id === req.params.id);
      if (product) {
        res.json({
          ...product,
          formattedPrice: formatCurrency(product.price, product.currency)
        });
      } else {
        res.status(404).json({ message: 'Product not found' });
      }
    });
    
    app.post('/api/products', (req, res) => {
      const newProduct: Product = {
        id: `p${products.length + 1}`,
        name: req.body.name,
        price: req.body.price,
        description: req.body.description,
        currency: req.body.currency || 'USD'
      };
      products.push(newProduct);
      res.status(201).json({ message: 'Product added successfully', product: newProduct });
    });
    
    
    const port = process.env.PORT || 3000;
    const server = app.listen(port, () => {
      console.log(`Listening at http://localhost:${port}/api`);
    });
    server.on('error', console.error);
    
  2. Serve my-express-api:

    nx serve my-express-api
    
  3. Test the API:

    • GET Products: Open http://localhost:3000/api/products in your browser. Expected Output:

      [
        {
          "id": "p1",
          "name": "Laptop",
          "price": 1200,
          "description": "Powerful portable computer",
          "currency": "USD",
          "formattedPrice": "$1,200.00"
        },
        {
          "id": "p2",
          "name": "Keyboard",
          "price": 75.5,
          "description": "Mechanical RGB keyboard",
          "currency": "USD",
          "formattedPrice": "$75.50"
        },
        {
          "id": "p3",
          "name": "Mouse",
          "price": 30,
          "description": "Ergonomic wireless mouse",
          "currency": "EUR",
          "formattedPrice": "€30.00"
        }
      ]
      

      Notice how formattedPrice is added by our shared utility function!

    • POST New Product: Use Postman/Insomnia/curl to send a POST request.

      • Method: POST
      • URL: http://localhost:3000/api/products
      • Headers: Content-Type: application/json
      • Body (raw JSON):
        {
          "name": "Webcam",
          "price": 49.99,
          "description": "HD Webcam for video calls",
          "currency": "USD"
        }
        

      Expected Response (Status 201 Created):

      {
        "message": "Product added successfully",
        "product": {
          "id": "p4",
          "name": "Webcam",
          "price": 49.99,
          "description": "HD Webcam for video calls",
          "currency": "USD"
        }
      }
      

      If you GET /api/products again, you’ll see your new product.

    • Press Ctrl+C to stop the API server.

Exercise 3.3.3: Use the Data Models Library in my-angular-app

  1. Open apps/my-angular-app/src/app/app.component.ts and modify it:

    // apps/my-angular-app/src/app/app.component.ts
    import { Component, OnInit } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { RouterModule } from '@angular/router';
    import { Product, formatCurrency } from '@my-nx-org/shared/data-models'; // <--- Import shared types and utility
    
    @Component({
      standalone: true,
      imports: [CommonModule, RouterModule],
      selector: 'my-nx-org-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.scss'],
    })
    export class AppComponent implements OnInit {
      title = 'my-angular-app';
      products: Product[] = [];
      loading = true;
      error: string | null = null;
    
      ngOnInit() {
        this.fetchProducts();
      }
    
      async fetchProducts() {
        try {
          // This will fail unless the API is running and proxied. We'll set up proxy next.
          const response = await fetch('http://localhost:3000/api/products');
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }
          const data = await response.json();
          this.products = data;
        } catch (e: any) {
          this.error = `Failed to fetch products: ${e.message}`;
        } finally {
          this.loading = false;
        }
      }
    
      // Use the shared utility function to format price
      getFormattedPrice(product: Product): string {
        return formatCurrency(product.price, product.currency);
      }
    
      // Example of an Angular event handler
      viewDetails(product: Product) {
        alert(`Viewing details for ${product.name}`);
      }
    }
    
  2. Open apps/my-angular-app/src/app/app.component.html and modify it:

    <!-- apps/my-angular-app/src/app/app.component.html -->
    <div class="angular-app-container">
      <header>
        <h1>{{ title }}</h1>
        <nav>
          <a routerLink="/">Home</a>
          <a routerLink="/products">Products</a>
          <!-- Add more links as your app grows -->
        </nav>
      </header>
    
      <main>
        <h2>Product Catalog</h2>
    
        <div *ngIf="loading">
          Loading products...
        </div>
    
        <div *ngIf="error">
          <p class="error-message">Error: {{ error }}</p>
          <button (click)="fetchProducts()">Retry</button>
        </div>
    
        <div class="product-list" *ngIf="!loading && !error">
          <div class="product-card" *ngFor="let product of products">
            <h3>{{ product.name }}</h3>
            <p>{{ product.description }}</p>
            <div class="price">Price: {{ getFormattedPrice(product) }}</div>
            <button (click)="viewDetails(product)">View Details</button>
          </div>
        </div>
      </main>
    
      <router-outlet></router-outlet>
    </div>
    
  3. Open apps/my-angular-app/src/app/app.component.scss and add some basic styling:

    /* apps/my-angular-app/src/app/app.component.scss */
    .angular-app-container {
      font-family: Arial, sans-serif;
      padding: 20px;
      background-color: #f8f9fa;
      min-height: 100vh;
    }
    
    header {
      background-color: #e9ecef;
      padding: 15px 20px;
      border-radius: 8px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20px;
    
      h1 {
        margin: 0;
        color: #343a40;
      }
    
      nav a {
        margin-left: 15px;
        text-decoration: none;
        color: #007bff;
    
        &:hover {
          text-decoration: underline;
        }
      }
    }
    
    .main {
      background-color: #ffffff;
      padding: 25px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    
      h2 {
        color: #343a40;
        border-bottom: 1px solid #dee2e6;
        padding-bottom: 10px;
        margin-bottom: 20px;
      }
    }
    
    .error-message {
      color: #dc3545;
      font-weight: bold;
    }
    
    .product-list {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 20px;
    }
    
    .product-card {
      border: 1px solid #dee2e6;
      border-radius: 8px;
      padding: 15px;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
    
      h3 {
        margin-top: 0;
        color: #007bff;
        font-size: 1.4em;
      }
    
      p {
        color: #6c757d;
        flex-grow: 1;
      }
    
      .price {
        font-size: 1.2em;
        font-weight: bold;
        color: #28a745;
        margin: 10px 0;
      }
    
      button {
        background-color: #007bff;
        color: white;
        border: none;
        padding: 8px 15px;
        border-radius: 5px;
        cursor: pointer;
        font-size: 0.9em;
        transition: background-color 0.2s ease-in-out;
    
        &:hover {
          background-color: #0056b3;
        }
      }
    }
    
  4. Configure Proxy for API (Crucial for local development): For your Angular app to talk to your Node.js API (which is on localhost:3000), you need to tell Angular’s development server to proxy requests.

    • Create a new file: apps/my-angular-app/proxy.conf.json

      // apps/my-angular-app/proxy.conf.json
      {
        "/api": {
          "target": "http://localhost:3000",
          "secure": false,
          "changeOrigin": true
        }
      }
      
      • This tells Angular: “If you get a request to /api (like /api/products), send it to http://localhost:3000 instead.”
    • Update apps/my-angular-app/project.json to use this proxy configuration. Find the serve target and add proxyConfig to its options.

      Expected apps/my-angular-app/project.json (partial, locate serve target):

      // apps/my-angular-app/project.json
      {
        "targets": {
          "build": { /* ... */ },
          "serve": {
            "executor": "@nx/angular:dev-server",
            "options": {
              "buildTarget": "my-angular-app:build",
              "proxyConfig": "apps/my-angular-app/proxy.conf.json" // <--- ADD THIS LINE
            },
            "configurations": { /* ... */ }
          },
          "extract-i18n": { /* ... */ },
          "lint": { /* ... */ },
          "test": { /* ... */ }
        }
      }
      
  5. Run both the API and Angular App simultaneously:

    • Open two separate terminal tabs.
    • In Tab 1 (for API):
      nx serve my-express-api
      
    • In Tab 2 (for Angular app):
      nx serve my-angular-app
      
    • Go to http://localhost:4200 (or the port assigned to Angular).
    • You should now see your Angular app fetching and displaying products from your Node.js API, using the shared Product interface and formatCurrency utility.

3.4. The Magic of nx affected Commands

This is arguably Nx’s most powerful feature for large monorepos and efficient CI/CD. nx affected allows you to run tasks (build, test, lint) only on projects that have been impacted by your recent changes.

How it works:

  1. Nx compares your current code state to a base (e.g., your main branch or a previous commit).
  2. It uses the dependency graph (remember nx graph?) to intelligently figure out which files have changed and which projects consume those changes.
  3. It then executes the specified target only for those “affected” projects.

Exercise 3.4.1: See affected in action

  1. Stop all running servers (Ctrl+C in both terminal tabs).

  2. Make a small, isolated change:

    • Open libs/shared/data/data-models/src/lib/data-models.ts.
    • Add a new interface for Order:
      // libs/shared/data/data-models/src/lib/data-models.ts
      export interface Product { /* ... */ }
      export interface User { /* ... */ }
      export function formatCurrency(amount: number, currency: string = 'USD'): string { /* ... */ }
      
      // --- NEW CODE BELOW ---
      export interface OrderItem {
        productId: string;
        quantity: number;
      }
      
      export interface Order {
        id: string;
        userId: string;
        items: OrderItem[];
        totalAmount: number;
        status: 'pending' | 'shipped' | 'delivered';
        createdAt: string;
      }
      
    • Save the file. Do NOT commit your changes yet.
  3. View affected projects on the graph:

    nx affected:graph
    

    Expected Output (browser opens, graph highlights):

    • In the interactive graph, you should see data-models highlighted in a distinct color. More importantly, you’ll see my-express-api, my-react-app, and my-angular-app also highlighted!
    • Why? Because all three applications import from data-models, any change to data-models affects them, even if they don’t directly use the new Order interface yet. Nx is smart enough to know this dependency.
  4. Run tests for affected projects:

    nx affected --target=test
    

    Expected Output (only tests for data-models, my-express-api, my-react-app, my-angular-app run):

    NX Running target test for 4 projects:
    - data-models
    - my-react-app
    - my-angular-app
    - my-express-api
    
    ... (Jest/Karma test runner logs for each of these projects) ...
    
    NX Ran target test for 4 projects.
    
    • Imagine a monorepo with 50 projects. If only 4 are affected, Nx saves you the time of running 46 unnecessary test suites!
  5. Build affected projects:

    nx affected --target=build
    

    Expected Output (builds only data-models, my-express-api, my-react-app, my-angular-app):

    NX Running target build for 4 projects:
    - data-models
    - my-react-app
    - my-angular-app
    - my-express-api
    
    ... (build logs for each project) ...
    
    NX Ran target build for 4 projects.
    
  6. Commit your changes:

    git add .
    git commit -m "feat: Add Order interface to shared data models"
    
  7. Run nx affected:graph again:

    nx affected:graph
    
    • Now, no projects should be highlighted (assuming you have no other uncommitted changes). Nx sees that your codebase is in sync with the last commit on your main branch. This is the beauty of caching!

4. Advanced Topics and Best Practices

As your monorepo matures, you’ll want to leverage more sophisticated Nx features to maintain order and scalability.

4.1. Enforcing Module Boundaries (Architectural Constraints)

One of the biggest challenges in a monorepo can be maintaining a clean architecture. Developers might accidentally import a UI component from a backend utility library, leading to circular dependencies or architectural violations. Nx’s module boundary rules prevent this.

Recall: When we generated our libraries, we added --tags="scope:shared,type:ui" and --tags="scope:shared,type:data". Now we’ll use them.

  1. Open nx.json in your workspace.

  2. Add enforceModuleBoundaries rules under the plugins array (or anywhere near the top-level properties):

    // nx.json (add this section)
    {
      "$schema": "./node_modules/nx/schemas/nx-schema.json",
      // ... existing configurations ...
      "plugins": [
        // ... existing plugins ...
      ],
      "enforceModuleBoundaries": [
        {
          "sourceTag": "type:app",
          "onlyDependOnLibsWithTags": ["type:ui", "type:data", "scope:shared"]
        },
        {
          "sourceTag": "type:ui",
          "onlyDependOnLibsWithTags": ["type:util", "type:data", "scope:shared"] // UI libs can only use utilities, data, or other shared libs
        },
        {
          "sourceTag": "type:data",
          "onlyDependOnLibsWithTags": ["type:util", "scope:shared"] // Data libs can only use utilities or other shared libs
        },
        {
          "sourceTag": "scope:shared",
          "onlyDependOnLibsWithTags": ["scope:shared"] // Shared libs should only depend on other shared libs
        }
      ]
    }
    
    • sourceTag: The tag of the project doing the importing.
    • onlyDependOnLibsWithTags: A list of tags that libraries can have if they are to be imported by the sourceTag project.

    Interpretation of our rules:

    • type:app (e.g., my-react-app, my-angular-app, my-express-api): Can depend on type:ui, type:data, or any scope:shared library. This is generally flexible for applications.
    • type:ui (e.g., ui-components): Can only depend on libraries tagged type:util (if we had one), type:data (like data-models), or other scope:shared libraries. It cannot depend on an app or a feature-specific library.
    • type:data (e.g., data-models): Can only depend on type:util or scope:shared. It should be very low-level and not depend on UI or application logic.
    • scope:shared: Enforces that shared libraries only depend on other shared libraries, preventing them from pulling in specific application or feature concerns.
  3. Trigger a module boundary violation (on purpose!):

    • Open libs/shared/ui/ui-components/src/lib/button/button.tsx.
    • Add an intentional incorrect import from my-react-app (this will cause an error).
      // libs/shared/ui/ui-components/src/lib/button/button.tsx
      import styles from './button.module.css';
      import { App } from 'apps/my-react-app/src/app/app'; // <--- BAD IMPORT!
      
      export interface ButtonProps { /* ... */ }
      
      export function Button(props: ButtonProps) {
        // You could even try to use App here, but lint will catch the import
        console.log(App);
        const { label, onClick, variant = 'primary' } = props;
        return (
          <button className={`${styles['container']} ${styles[variant]}`} onClick={onClick}>
            {label}
          </button>
        );
      }
      
      export default Button;
      
    • Save the file.
  4. Run the lint command for the affected project:

    nx lint ui-components
    

    Expected Output (ERROR!):

    NX Running target lint for project ui-components
    
    Linting "ui-components"...
    
    ERROR: You are not allowed to import from apps/my-react-app from libs/shared/ui/ui-components.
    A project tagged with "type:ui" can only depend on libs with tags "type:util", "type:data", "scope:shared".
    apps/my-react-app is not tagged with any of these.
    
    ... (ESLint errors related to the import) ...
    
    NX  Command failed with exit code 1.
    
    • Success! Nx caught our architectural violation. This is incredibly powerful for maintaining discipline in a large monorepo.
  5. Remove the bad import from libs/shared/ui/ui-components/src/lib/button/button.tsx and save the file to fix the error.

    --- a/libs/shared/ui/ui-components/src/lib/button/button.tsx
    +++ b/libs/shared/ui/ui-components/src/lib/button/button.tsx
    @@ -1,6 +1,5 @@
     import styles from './button.module.css';
    -import { App } from 'apps/my-react-app/src/app/app'; // <--- BAD IMPORT!
    
     /* eslint-disable-next-line */
     export interface ButtonProps {
    @@ -10,7 +9,6 @@
     export function Button(props: ButtonProps) {
       // You could even try to use App here, but lint will catch the import
    -  console.log(App);
       const { label, onClick, variant = 'primary' } = props;
       return (
         <button className={`${styles['container']} ${styles[variant]}`} onClick={onClick}>
    
    • Run nx lint ui-components again, and it should now pass.

4.2. Nx Release (Versioning and Publishing)

For libraries you might want to publish to npm (either public or private), Nx provides a unified nx release command. It handles versioning, changelog generation, and publishing.

  • We marked ui-components with --publishable when generating it, which created a package.json inside its folder (libs/shared/ui/ui-components/package.json).
  1. View release information:
    nx release version --dry-run
    
    Expected Output (shows what versions would be bumped):
    NX Performing release versioning...
    
    NX   These projects will be versioned:
    
    - my-react-app: 0.0.1 -> 0.0.2 (major)
    - my-angular-app: 0.0.1 -> 0.0.2 (major)
    - my-express-api: 0.0.1 -> 0.0.2 (major)
    - ui-components: 0.0.1 -> 0.0.2 (major)
    - data-models: 0.0.1 -> 0.0.2 (major)
    
    NX   These projects have no changelog or are not explicitly versioned:
    - (root)
    
    NX Performing release changelog generation...
    
    NX Performing release git operations...
    
    NX Releasing commit: HEAD
    NX Release commit message: 'chore(release): publish apps and libs'
    NX Creating tag: v0.0.2
    
    NX Performing release publishing...
    
    NX PUBLISHING:
    
    - ui-components: 0.0.2
    - data-models: 0.0.2
    
    Note: The "release" command is not connected to any git remote.
    Run "nx connect" to enable remote caching and distribution for your Nx Workspace!
    
    • The dry-run is crucial! It shows you what would happen without actually making changes.
    • It detects my-react-app etc. are apps, and ui-components, data-models are libraries ready for potential publishing. It suggests version bumps for all projects.
    • nx release is highly configurable (e.g., independent versioning, custom changelog formats). This is a great topic for advanced learning.

4.3. Nx Cloud (Remote Caching & Distributed Task Execution)

You chose “No” for Nx Cloud during setup, but it’s vital for teams.

  • Remote Caching: Builds/tests run by one team member (or CI server) are cached remotely. If another team member or CI run tries the exact same task on the exact same code, Nx downloads the cached result instead of re-running the task, saving massive time.
  • Distributed Task Execution (DTE): For huge monorepos, DTE allows Nx to split your build/test tasks across multiple machines in your CI pipeline, drastically reducing overall execution time.
  1. To connect to Nx Cloud (if you decide to later):
    nx connect
    
    Follow the prompts. It’s free for open source and small teams.

4.4. Best Practices for Large Monorepos

  • Modularity over Monolith: Break down your codebase into small, well-defined libraries. Each library should have a single responsibility.
  • Clear Boundaries: Use module boundary rules (enforceModuleBoundaries in nx.json) rigorously.
  • Consistent Tooling: Leverage Nx’s generators and targetDefaults to ensure all projects use the same versions of linters, test runners, and build tools.
  • Automated Testing (and nx affected): Ensure all libraries and applications have robust test suites. Use nx affected --target=test in your CI pipeline to only run relevant tests.
  • nx graph Often: Regularly visualize your dependency graph (nx graph) to catch unintended dependencies early.
  • Regular Nx Updates: Keep Nx up-to-date using nx migrate latest to benefit from performance improvements, bug fixes, and new features.

5. Guided Projects: Building an End-to-End E-Commerce Monorepo

Now for the grand finale! We’ll build a simplified e-commerce platform within our my-nx-org monorepo. This project will truly highlight the benefits of shared code.

Project Objective: A basic e-commerce system with:

  • A React Admin Panel (admin-panel)
  • An Angular Storefront (storefront-app)
  • A Node.js Express Backend API (api-server)
  • A Shared UI Library (shared-ui) - containing common React components.
  • A Shared Domain Library (ecom-domain) - containing shared types and utilities.

Prerequisites: You should have followed all previous steps, including installing @nx/react, @nx/angular, @nx/node, and @nx/js. Ensure your workspace is clean of previous temporary projects (my-react-app, etc.) or create a new one:

Option A: Clean up existing workspace

# From my-nx-org directory
rm -rf apps/* libs/*
git add .
git commit -m "chore: Clean up for guided project" # Or simply reset if you don't care about history

Option B: Start a fresh workspace

cd .. # Go up one directory
rm -rf my-nx-org # Delete the old one
npx create-nx-workspace@latest ecom-monorepo # Create a new one
cd ecom-monorepo
npm install -D @nx/react @nx/angular @nx/node @nx/js # Install plugins again

For this guided project, I’ll assume you chose Option B and your workspace is named ecom-monorepo.

Project Setup Steps:

  1. Generate Core Applications:

    • React Admin Panel:
      nx g @nx/react:app admin-panel --style=css --routing --tags="scope:admin,type:app"
      
    • Angular Storefront:
      nx g @nx/angular:app storefront-app --style=scss --routing --standalone --tags="scope:store,type:app"
      
    • Node.js Express Backend API:
      nx g @nx/node:app api-server --framework=express --tags="scope:backend,type:app"
      
    • Check the graph: nx graph (you’ll see three isolated app projects)
  2. Generate Shared Libraries:

    • E-Commerce Domain Library (shared types & utilities):

      nx g @nx/js:lib ecom-domain --directory=shared --importPath=@ecom/domain --tags="scope:shared,type:data"
      
      • Edit libs/shared/ecom-domain/src/lib/ecom-domain.ts:
        // libs/shared/ecom-domain/src/lib/ecom-domain.ts
        
        export interface Product {
          id: string;
          name: string;
          price: number;
          currency: string;
          description?: string;
          imageUrl?: string;
        }
        
        export interface Order {
          id: string;
          userId: string;
          items: { productId: string; quantity: number; priceAtOrder: number }[];
          totalAmount: number;
          status: 'pending' | 'shipped' | 'delivered' | 'cancelled';
          createdAt: string;
        }
        
        export function formatPrice(amount: number, currency: string = 'USD'): string {
          return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: currency,
          }).format(amount);
        }
        
      • Edit libs/shared/ecom-domain/src/index.ts to export:
        // libs/shared/ecom-domain/src/index.ts
        export * from './lib/ecom-domain';
        
    • Shared UI Library (React Components):

      nx g @nx/react:lib shared-ui --directory=shared --buildable --publishable --importPath=@ecom/shared-ui --tags="scope:shared,type:ui"
      
      • Generate Button component in shared-ui:

        nx g @nx/react:component button --project=shared-ui --export
        
        • Edit libs/shared/shared-ui/src/lib/button/button.tsx:
          // libs/shared/shared-ui/src/lib/button/button.tsx
          import './button.module.css';
          
          export interface ButtonProps {
            text: string;
            onClick: () => void;
            variant?: 'primary' | 'secondary' | 'danger';
            disabled?: boolean;
          }
          
          export function Button({ text, onClick, variant = 'primary', disabled = false }: ButtonProps) {
            const className = `button ${variant}`;
            return (
              <button className={className} onClick={onClick} disabled={disabled}>
                {text}
              </button>
            );
          }
          
          export default Button;
          
        • Edit libs/shared/shared-ui/src/lib/button/button.module.css:
          /* libs/shared/shared-ui/src/lib/button/button.module.css */
          .button {
            padding: 10px 15px;
            border-radius: 5px;
            border: none;
            cursor: pointer;
            font-size: 1rem;
            font-weight: 600;
            transition: background-color 0.2s ease, transform 0.1s ease;
            margin: 0 5px; /* Add some margin */
          }
          
          .button:hover:not(:disabled) {
            transform: translateY(-1px);
          }
          
          .button:disabled {
            cursor: not-allowed;
            opacity: 0.6;
          }
          
          .primary {
            background-color: #007bff;
            color: white;
          }
          
          .primary:hover:not(:disabled) {
            background-color: #0056b3;
          }
          
          .secondary {
            background-color: #6c757d;
            color: white;
          }
          
          .secondary:hover:not(:disabled) {
            background-color: #5a6268;
          }
          
          .danger {
            background-color: #dc3545;
            color: white;
          }
          
          .danger:hover:not(:disabled) {
            background-color: #c82333;
          }
          
      • Generate Card component in shared-ui:

        nx g @nx/react:component card --project=shared-ui --export
        
        • Edit libs/shared/shared-ui/src/lib/card/card.tsx:
          // libs/shared/shared-ui/src/lib/card/card.tsx
          import React from 'react';
          import './card.module.css';
          
          export interface CardProps {
            title?: string;
            children: React.ReactNode;
          }
          
          export function Card({ title, children }: CardProps) {
            return (
              <div className="card">
                {title && <h3 className="card-title">{title}</h3>}
                <div className="card-content">{children}</div>
              </div>
            );
          }
          
          export default Card;
          
        • Edit libs/shared/shared-ui/src/lib/card/card.module.css:
          /* libs/shared/shared-ui/src/lib/card/card.module.css */
          .card {
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            padding: 20px;
            margin-bottom: 20px;
            border: 1px solid #e0e0e0;
          }
          
          .card-title {
            margin-top: 0;
            color: #333;
            font-size: 1.5rem;
            border-bottom: 1px solid #eee;
            padding-bottom: 10px;
            margin-bottom: 15px;
          }
          
          .card-content {
            color: #555;
          }
          
  3. Implement the Node.js API (api-server):

    • Edit apps/api-server/src/main.ts to use Product & Order from @ecom/domain:

      // apps/api-server/src/main.ts
      import express from 'express';
      import * as path from 'path';
      import { Product, Order, formatPrice } from '@ecom/domain';
      
      const app = express();
      app.use(express.json()); // Enable JSON body parsing
      
      // Serve static assets (e.g., from your Angular/React app later, or a simple index.html)
      app.use('/assets', express.static(path.join(__dirname, 'assets')));
      
      // In-memory data store (for simplicity in this example)
      let products: Product[] = [
        {
          id: 'prod1', name: 'Smartwatch X', price: 299.99, currency: 'USD',
          description: 'Advanced smartwatch with health tracking.', imageUrl: 'https://via.placeholder.com/150/007bff/FFFFFF?text=Smartwatch'
        },
        {
          id: 'prod2', name: 'Wireless Earbuds', price: 129.00, currency: 'USD',
          description: 'High-fidelity audio with active noise cancellation.', imageUrl: 'https://via.placeholder.com/150/28a745/FFFFFF?text=Earbuds'
        },
        {
          id: 'prod3', name: 'Portable Charger 10000mAh', price: 35.50, currency: 'USD',
          description: 'Fast charging for all your devices.', imageUrl: 'https://via.placeholder.com/150/ffc107/343a40?text=Charger'
        },
        {
          id: 'prod4', name: 'Ergonomic Keyboard', price: 89.99, currency: 'EUR',
          description: 'Mechanical keyboard designed for comfort.', imageUrl: 'https://via.placeholder.com/150/17a2b8/FFFFFF?text=Keyboard'
        },
      ];
      
      let orders: Order[] = [];
      let orderIdCounter = 1;
      
      // Routes
      app.get('/api', (req, res) => {
        res.send({ message: 'Welcome to the E-Commerce API!' });
      });
      
      app.get('/api/products', (req, res) => {
        res.json(products.map(p => ({
          ...p,
          formattedPrice: formatPrice(p.price, p.currency)
        })));
      });
      
      app.get('/api/products/:id', (req, res) => {
        const product = products.find(p => p.id === req.params.id);
        if (product) {
          res.json({ ...product, formattedPrice: formatPrice(product.price, product.currency) });
        } else {
          res.status(404).json({ message: 'Product not found' });
        }
      });
      
      app.post('/api/products', (req, res) => {
        const { name, price, description, currency = 'USD', imageUrl } = req.body;
        if (!name || !price || !description) {
          return res.status(400).json({ message: 'Missing required product fields.' });
        }
        const newProduct: Product = {
          id: `prod${products.length + 1}`,
          name,
          price,
          description,
          currency,
          imageUrl
        };
        products.push(newProduct);
        res.status(201).json({ message: 'Product added!', product: newProduct });
      });
      
      app.get('/api/orders', (req, res) => {
        res.json(orders);
      });
      
      app.post('/api/orders', (req, res) => {
        const { userId, items } = req.body; // items: [{ productId, quantity }]
        if (!userId || !items || !Array.isArray(items) || items.length === 0) {
          return res.status(400).json({ message: 'Invalid order data.' });
        }
      
        let totalAmount = 0;
        const orderItems: { productId: string; quantity: number; priceAtOrder: number }[] = [];
      
        try {
          items.forEach((item: { productId: string; quantity: number }) => {
            const product = products.find(p => p.id === item.productId);
            if (!product) {
              throw new Error(`Product with ID ${item.productId} not found.`);
            }
            orderItems.push({ ...item, priceAtOrder: product.price });
            totalAmount += product.price * item.quantity;
          });
        } catch (error: any) {
          return res.status(404).json({ message: error.message });
        }
      
        const newOrder: Order = {
          id: `order${orderIdCounter++}`,
          userId,
          items: orderItems,
          totalAmount,
          status: 'pending',
          createdAt: new Date().toISOString(),
        };
        orders.push(newOrder);
        res.status(201).json({ message: 'Order placed successfully!', order: newOrder });
      });
      
      
      const port = process.env.PORT || 3333;
      const server = app.listen(port, () => {
        console.log(`Listening at http://localhost:${port}/api`);
      });
      server.on('error', console.error);
      
    • Run the API server:

      nx serve api-server
      

      (Keep this running in a dedicated terminal tab.)

  4. Implement the React Admin Panel (admin-panel):

    • Edit apps/admin-panel/src/app/app.tsx:

      // apps/admin-panel/src/app/app.tsx
      import { useEffect, useState } from 'react';
      import { Button, Card } from '@ecom/shared-ui'; // Import shared UI components
      import { Product, Order, formatPrice } from '@ecom/domain'; // Import shared types & utilities
      import './app.module.css'; // Add some app-specific styling
      
      export function App() {
        const [products, setProducts] = useState<Product[]>([]);
        const [orders, setOrders] = useState<Order[]>([]);
        const [loadingProducts, setLoadingProducts] = useState(true);
        const [loadingOrders, setLoadingOrders] = useState(true);
        const [error, setError] = useState<string | null>(null);
      
        const fetchProducts = async () => {
          try {
            const response = await fetch('/api/products');
            if (!response.ok) throw new Error('Failed to fetch products');
            const data = await response.json();
            setProducts(data);
          } catch (err: any) {
            setError(err.message);
          } finally {
            setLoadingProducts(false);
          }
        };
      
        const fetchOrders = async () => {
          try {
            const response = await fetch('/api/orders');
            if (!response.ok) throw new Error('Failed to fetch orders');
            const data = await response.json();
            setOrders(data);
          } catch (err: any) {
            setError(err.message);
          } finally {
            setLoadingOrders(false);
          }
        };
      
        useEffect(() => {
          fetchProducts();
          fetchOrders();
        }, []);
      
        const handleAddProduct = () => alert('Add Product logic here!');
        const handleUpdateOrder = (orderId: string) => alert(`Update order ${orderId} logic here!`);
      
        if (loadingProducts || loadingOrders) return <div className="loading">Loading data...</div>;
        if (error) return <div className="error">Error: {error}</div>;
      
        return (
          <div className="admin-layout">
            <header className="admin-header">
              <h1>E-Commerce Admin Panel</h1>
              <Button text="Add New Product" onClick={handleAddProduct} variant="primary" />
            </header>
      
            <main className="admin-main">
              <section>
                <h2>Products Overview</h2>
                <div className="card-grid">
                  {products.map(product => (
                    <Card key={product.id} title={product.name}>
                      <img src={product.imageUrl} alt={product.name} className="product-image" />
                      <p>{product.description}</p>
                      <p>Price: <strong>{product.formattedPrice}</strong></p>
                      <Button text="Edit" onClick={() => alert(`Edit ${product.name}`)} variant="secondary" />
                      <Button text="Delete" onClick={() => alert(`Delete ${product.name}`)} variant="danger" />
                    </Card>
                  ))}
                </div>
              </section>
      
              <section>
                <h2>Recent Orders</h2>
                <div className="card-grid">
                  {orders.length === 0 && <p>No orders yet.</p>}
                  {orders.map(order => (
                    <Card key={order.id} title={`Order #${order.id}`}>
                      <p>User ID: {order.userId}</p>
                      <p>Total: <strong>{formatPrice(order.totalAmount, 'USD')}</strong></p>
                      <p>Status: <span className={`order-status ${order.status}`}>{order.status.toUpperCase()}</span></p>
                      <p>Items: {order.items.length}</p>
                      <Button text="View Details" onClick={() => handleUpdateOrder(order.id)} variant="secondary" />
                    </Card>
                  ))}
                </div>
              </section>
            </main>
          </div>
        );
      }
      
      export default App;
      
    • Edit apps/admin-panel/src/app/app.module.css (for basic styling):

      /* apps/admin-panel/src/app/app.module.css */
      body {
        margin: 0;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
          'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
          sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
      }
      
      .admin-layout {
        background-color: #f0f2f5;
        min-height: 100vh;
        padding: 20px;
      }
      
      .admin-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        background-color: #ffffff;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        margin-bottom: 30px;
      }
      
      .admin-header h1 {
        color: #333;
        margin: 0;
      }
      
      .admin-main {
        padding: 20px;
        background-color: #ffffff;
        border-radius: 8px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      }
      
      .admin-main section {
        margin-bottom: 40px;
      }
      
      .admin-main h2 {
        color: #333;
        border-bottom: 2px solid #eee;
        padding-bottom: 10px;
        margin-bottom: 25px;
        font-size: 1.8rem;
      }
      
      .card-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
        gap: 20px;
      }
      
      .product-image {
        max-width: 100%;
        height: auto;
        border-radius: 4px;
        margin-bottom: 15px;
      }
      
      .order-status {
        font-weight: bold;
        padding: 5px 10px;
        border-radius: 4px;
        display: inline-block;
      }
      
      .order-status.pending {
        background-color: #ffc107;
        color: #343a40;
      }
      .order-status.shipped {
        background-color: #17a2b8;
        color: white;
      }
      .order-status.delivered {
        background-color: #28a745;
        color: white;
      }
      .order-status.cancelled {
        background-color: #dc3545;
        color: white;
      }
      
    • Configure Proxy for API: Create apps/admin-panel/proxy.conf.json:

      // apps/admin-panel/proxy.conf.json
      {
        "/api": {
          "target": "http://localhost:3333",
          "secure": false,
          "changeOrigin": true
        }
      }
      
      • Update apps/admin-panel/project.json under the serve target to use this proxy config.
        // apps/admin-panel/project.json (partial)
        {
          "targets": {
            "serve": {
              "executor": "@nx/webpack:dev-server", // or similar, depending on your Nx React setup
              "options": {
                "buildTarget": "admin-panel:build",
                "hmr": true,
                "proxyConfig": "apps/admin-panel/proxy.conf.json" // <--- ADD THIS LINE
              },
              // ...
            }
          }
        }
        
    • Run the React Admin Panel:

      nx serve admin-panel
      

      (Keep this running in another terminal tab.)

  5. Implement the Angular Storefront (storefront-app):

    • Edit apps/storefront-app/src/app/app.component.ts:

      // apps/storefront-app/src/app/app.component.ts
      import { Component, OnInit } from '@angular/core';
      import { CommonModule } from '@angular/common';
      import { RouterModule } from '@angular/router';
      import { Product, Order, formatPrice } from '@ecom/domain'; // Import shared types & utilities
      
      @Component({
        standalone: true,
        imports: [CommonModule, RouterModule],
        selector: 'ecom-monorepo-root',
        templateUrl: './app.component.html',
        styleUrls: ['./app.component.scss'],
      })
      export class AppComponent implements OnInit {
        title = 'E-Commerce Storefront';
        products: Product[] = [];
        cartItems: Product[] = [];
        loading = true;
        error: string | null = null;
      
        ngOnInit() {
          this.fetchProducts();
        }
      
        async fetchProducts() {
          try {
            const response = await fetch('/api/products'); // Uses proxy
            if (!response.ok) {
              throw new Error(`HTTP error! status: ${response.status}`);
            }
            const data = await response.json();
            this.products = data;
          } catch (e: any) {
            this.error = `Failed to fetch products: ${e.message}`;
          } finally {
            this.loading = false;
          }
        }
      
        getFormattedPrice(product: Product): string {
          return formatPrice(product.price, product.currency);
        }
      
        addToCart(product: Product) {
          this.cartItems.push(product);
          alert(`${product.name} added to cart!`);
          console.log('Cart:', this.cartItems);
        }
      
        async checkout() {
          if (this.cartItems.length === 0) {
            alert('Your cart is empty!');
            return;
          }
      
          const itemsForOrder = this.cartItems.map(item => ({ productId: item.id, quantity: 1 })); // Simplistic: 1 quantity for each
          const totalAmount = this.cartItems.reduce((sum, item) => sum + item.price, 0);
      
          try {
            const response = await fetch('/api/orders', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                userId: 'user-123', // Dummy user ID
                items: itemsForOrder,
              }),
            });
      
            if (!response.ok) {
              const errorData = await response.json();
              throw new Error(errorData.message || 'Failed to place order');
            }
      
            const orderResponse = await response.json();
            alert(`Order placed successfully! Order ID: ${orderResponse.order.id}`);
            this.cartItems = []; // Clear cart
            // You might want to refresh orders in admin-panel here
          } catch (e: any) {
            alert(`Checkout failed: ${e.message}`);
          }
        }
      }
      
    • Edit apps/storefront-app/src/app/app.component.html:

      <!-- apps/storefront-app/src/app/app.component.html -->
      <div class="storefront-layout">
        <header class="store-header">
          <h1>{{ title }}</h1>
          <nav>
            <a routerLink="/">Products</a>
            <span class="cart-info">
              Cart ({{ cartItems.length }})
              <button *ngIf="cartItems.length > 0" (click)="checkout()" class="checkout-button">Checkout</button>
            </span>
          </nav>
        </header>
      
        <main class="store-main">
          <h2>Our Products</h2>
      
          <div *ngIf="loading" class="loading">
            Loading products...
          </div>
      
          <div *ngIf="error" class="error">
            <p>Error: {{ error }}</p>
            <button (click)="fetchProducts()">Retry Loading</button>
          </div>
      
          <div class="product-grid" *ngIf="!loading && !error">
            <div class="product-card" *ngFor="let product of products">
              <img [src]="product.imageUrl" [alt]="product.name" class="product-image" />
              <h3>{{ product.name }}</h3>
              <p>{{ product.description }}</p>
              <div class="price">{{ getFormattedPrice(product) }}</div>
              <button (click)="addToCart(product)" class="add-to-cart-button">Add to Cart</button>
            </div>
          </div>
        </main>
      
        <router-outlet></router-outlet>
      </div>
      
    • Edit apps/storefront-app/src/app/app.component.scss (for basic styling):

      /* apps/storefront-app/src/app/app.component.scss */
      .storefront-layout {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        background-color: #e6e9ed;
        min-height: 100vh;
        padding: 20px;
      }
      
      .store-header {
        background-color: #343a40;
        color: white;
        padding: 20px;
        border-radius: 8px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 30px;
      
        h1 {
          margin: 0;
          font-size: 2.2em;
        }
      
        nav {
          display: flex;
          align-items: center;
        }
      
        nav a {
          color: white;
          margin-right: 20px;
          text-decoration: none;
          font-weight: bold;
      
          &:hover {
            text-decoration: underline;
          }
        }
      
        .cart-info {
          background-color: #007bff;
          padding: 8px 15px;
          border-radius: 5px;
          font-weight: bold;
          display: flex;
          align-items: center;
        }
        .checkout-button {
          background-color: #28a745;
          color: white;
          border: none;
          padding: 5px 10px;
          border-radius: 4px;
          cursor: pointer;
          margin-left: 10px;
          font-size: 0.9em;
          &:hover {
            background-color: #218838;
          }
        }
      }
      
      .store-main {
        background-color: #ffffff;
        padding: 30px;
        border-radius: 8px;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
      
        h2 {
          color: #333;
          border-bottom: 2px solid #ddd;
          padding-bottom: 10px;
          margin-bottom: 30px;
          font-size: 2em;
        }
      }
      
      .product-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
        gap: 25px;
      }
      
      .product-card {
        border: 1px solid #e0e0e0;
        border-radius: 10px;
        padding: 20px;
        background-color: #fff;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
      
        &:hover {
          transform: translateY(-5px);
          box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
        }
      
        .product-image {
          max-width: 100%;
          height: 150px;
          object-fit: contain;
          border-radius: 5px;
          margin-bottom: 15px;
        }
      
        h3 {
          color: #007bff;
          margin-top: 0;
          font-size: 1.5em;
          min-height: 40px; /* Ensure consistent height for titles */
        }
      
        p {
          color: #555;
          flex-grow: 1;
          margin-bottom: 15px;
        }
      
        .price {
          font-size: 1.3em;
          font-weight: bold;
          color: #28a745;
          margin-bottom: 15px;
        }
      
        .add-to-cart-button {
          background-color: #ffc107;
          color: #343a40;
          padding: 10px 15px;
          border: none;
          border-radius: 5px;
          cursor: pointer;
          font-size: 1em;
          font-weight: bold;
          transition: background-color 0.2s ease-in-out;
      
          &:hover {
            background-color: #e0a800;
          }
        }
      }
      
    • Configure Proxy for API: Create apps/storefront-app/proxy.conf.json:

      // apps/storefront-app/proxy.conf.json
      {
        "/api": {
          "target": "http://localhost:3333",
          "secure": false,
          "changeOrigin": true
        }
      }
      
      • Update apps/storefront-app/project.json under the serve target to use this proxy config.
        // apps/storefront-app/project.json (partial)
        {
          "targets": {
            "serve": {
              "executor": "@nx/angular:dev-server",
              "options": {
                "buildTarget": "storefront-app:build",
                "proxyConfig": "apps/storefront-app/proxy.conf.json" // <--- ADD THIS LINE
              },
              // ...
            }
          }
        }
        
    • Run the Angular Storefront:

      nx serve storefront-app
      

      (Keep this running in a third terminal tab.)

Witnessing the Benefits:

Now that all three applications (api-server, admin-panel, storefront-app) are running, interact with them:

  1. Open two browser tabs:

    • One for your React Admin Panel (likely http://localhost:4200)
    • One for your Angular Storefront (likely http://localhost:4201 or similar, check terminal output for exact port)
  2. Interact with the Storefront:

    • Browse the products.
    • Add some products to your cart.
    • Click “Checkout” – this will send a POST /api/orders request to your Node.js API.
  3. Check the Admin Panel:

    • Refresh the React Admin Panel. You should now see the order you just placed from the Angular Storefront reflected in the “Recent Orders” section!

This is the core power of an Nx Monorepo:

  • Seamless Shared Types: The Product and Order interfaces, along with the formatPrice utility, are defined once in libs/shared/ecom-domain and consumed by both your Node.js backend and your Angular and React frontends. Changes in one place automatically propagate and are type-checked everywhere.
  • Shared UI (for React): The React admin-panel is using Button and Card components from libs/shared/shared-ui. This centralizes your design system.
  • Unified Development Experience: You use nx serve to run Angular, React, and Node.js applications, abstracting away framework-specific commands.
  • Dependency Visualization: Run nx graph again. You’ll see a rich graph showing how all your apps depend on @ecom/domain, and how admin-panel also depends on @ecom/shared-ui.
  • Optimized Workflows: Imagine if you needed to add a new field to Product. You’d modify it in libs/shared/ecom-domain. Then, running nx affected --target=test would only test api-server, admin-panel, and storefront-app (and ecom-domain itself) because Nx understands their dependencies. This is incredibly efficient in a large organization.

6. Bonus Section: Further Learning and Resources

You’ve built a multi-application, multi-framework monorepo with shared code and witnessed the efficiency of Nx! That’s a huge achievement. To continue your journey and tackle even more complex scenarios, here are excellent resources.

  • Nx.Dev Official Tutorials: The official Nx website (nx.dev) is continuously updated with excellent, hands-on tutorials for various technologies.
    • Start with the “Getting Started” guides specific to React, Angular, Node.js if you want to deepen your understanding of each integration.
    • Look for “Recipes” which provide practical solutions to common monorepo challenges.
  • Nx Workshop: Nrwl (the creators of Nx) often hosts free, in-depth workshops. Keep an eye on their announcements.
  • Egghead.io / YouTube Nx Courses: Search for “Nx Workspace” on platforms like Egghead.io or YouTube. Many experts provide structured courses. Look for recent content (2024/2025).

Official Documentation:

Blogs and Articles:

  • Nx Blog: https://nx.dev/blog - Stay informed about new features, major updates, and best practices directly from the Nx team.
  • Nrwl Blog: https://blog.nrwl.io/nx/home - Additional articles and insights from the company behind Nx.
  • Community Blogs: Search platforms like Dev.to, Medium, and Hashnode for articles tagged “Nx Monorepo” or “Nrwl Nx” + “2025” for community-driven tutorials and experiences.

YouTube Channels:

  • Official Nx YouTube Channel (@NxDevtools): https://www.youtube.com/@NxDevtools - Excellent source for official tutorials, feature highlights, and conference talks.
  • Relevant Web Development Channels: Many general web development channels will occasionally cover Nx. Look for channels that focus on modern build tools, React, Angular, and Node.js.

Community Forums/Groups:

  • Nx Discord Server: https://go.nx.dev/community - An incredibly active community where you can ask questions, get help, and discuss with other Nx users and core team members.
  • Stack Overflow: Tag your questions with nx or nx-workspace.
  • GitHub Issues: The Nx GitHub repository is the place to report bugs, suggest features, and interact with the maintainers.

Next Steps/Advanced Topics:

Once you’ve mastered the content in this “beginner-friendly” guide, here’s what to explore next to become an Nx expert:

  • Custom Nx Generators and Executors: Learn to create your own tailored scaffolding tools and task runners for your specific organizational needs. This is powerful for enforcing domain-specific best practices.
  • Module Federation with Nx: Dive into building truly independent micro-frontends or microservices that can be deployed separately but still share code within the monorepo. Nx has state-of-the-art support for Module Federation.
  • Advanced CI/CD Pipelines: Implement Nx Cloud for remote caching and distributed task execution in your continuous integration and deployment pipelines to achieve lightning-fast CI builds.
  • Nx Release Strategies: Explore more complex versioning (e.g., independent vs. unified) and publishing workflows for your libraries.
  • Plugin Development: Understand how to build custom Nx plugins to integrate with entirely new technologies or specific internal tools.
  • Monorepo Health and Performance Tuning: Learn about techniques for maintaining the performance and health of very large monorepos, including deeper analysis of the dependency graph and build times.
  • Integrating with Non-JavaScript Technologies: Nx is expanding its support. Explore its capabilities for integrating with tools like Spring Boot (Java), Go, Rust, and more.

By delving into these advanced topics, you’ll be able to unlock the full potential of Nx Workspace and build, manage, and scale even the most complex software projects with confidence and efficiency. Keep building, keep learning!

For more Advance topics follow here -> Nx Workspace: Advanced Architectures & Production Mastery (Latest Version)