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:
Node.js (v20.19.0 or later): Nx relies on Node.js.
- Check your version: Open your terminal and run:Expected Output (or similar):
node -vv20.19.0 - If you need to install/update: Visit nodejs.org or use
nvm(Node Version Manager) for easier version switching.
- Check your version: Open your terminal and run:
npm (Node Package Manager): Comes bundled with Node.js.
- Check your version:Expected Output (or similar):
npm -v10.5.2
(Optional: You can also use Yarn or pnpm as your package manager. For this guide, we’ll stick with
npmcommands.)- Check your version:
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.
Open your terminal or command prompt.
Run the Nx workspace creation command:
npx create-nx-workspace@latest my-nx-orgnpx: Executes a Node.js package directly without global installation. This ensures you always use the latestcreate-nx-workspaceutility.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.
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) · NoWhich stack would you like to use? · None: This is crucial. SelectingNonegives 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 installedNavigate 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:Expected Output (or similar):
ls -F./ .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, oryarn.lockdepending on yournpmconfiguration.
- You might see
- Open the workspace in your code editor (e.g., VS 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.
code .
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 rootpackage.jsonfor your entire monorepo. It lists all the shared development and production dependencies. Nx cleverly uses your package manager’s workspace features (likenpm 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
Projects (Apps & Libraries): In Nx, every application and every library is a “project.” Each project has its own
project.jsonfile (or a configuration section innx.jsonfor 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.
- Applications (Apps): Runnable, deployable codebases (e.g.,
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 shorthandnx 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).
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.jsonfile under thetargetsproperty.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:developmentvs.serve:production).
Syntax:
nx run <project-name>:<target-name>[:<configuration>] [options]or the shorthandnx <target-name> <project-name> [options].
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 (
affectedcommands): 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
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
Open
nx.jsonin yourmy-nx-orgworkspace.code nx.jsonLook for the
tasksRunnerOptionsandtargetDefaultssections. These define how tasks are run and default settings for targets likebuild,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: trueis enabled by default forbuild,lint, andtest. This is Nx’s caching in action!dependsOnspecifies dependencies between targets (e.g.,buildmight depend onlint).
- Notice
Exercise 2.2: List installed plugins
- Run the
nx listcommand to see which Nx plugins are currently installed in your workspace.Expected Output (or similar):nx listNX 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.
- Initially, you’ll mostly see core Nx plugins (like
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.
Install the
@nx/reactplugin: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. ...Generate a new React application:
nx g @nx/react:app my-react-app --style=css --routing=truenx g: Shorthand fornx generate.@nx/react:app: We’re using theappgenerator from the@nx/reactplugin.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!
Explore the new React app:
- Look in your
apps/directory. You should now seeapps/my-react-app. - Open
apps/my-react-app/project.json. This file contains the configurations and targets specific tomy-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 varioustargetslikebuild,serve,lint, andtest, each with its specificexecutor.
- Look in your
Exercise 2.4: Serve your new React application
Let’s fire up your React app!
Run the serve command:
nx serve my-react-appnx serve: Shorthand fornx 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+Cin your terminal to stop the server.
Exercise 2.5: Visualize the dependency graph
This is where Nx starts to show its intelligence.
- Run the graph command:Expected Output (opens browser tab with graph):
nx graphNX 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.
- A new browser tab will open, displaying an interactive graph. You should see a single node for
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
Install the
@nx/angularplugin:npm install -D @nx/angularExpected Output (similar to React plugin install):
added 12 packages, and audited 13 packages in 1sGenerate 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.jsonupdated):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-appServe the Angular App:
nx serve my-angular-appExpected 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 hadmy-react-apprunning (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+Cto stop.
- Your browser should open
Exercise 3.1.2: Generate a Node.js (Express) API
Now for a backend!
Install the
@nx/nodeplugin:npm install -D @nx/nodeGenerate 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 supportsnestfor NestJS, ornonefor 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-apiServe the Node.js API:
nx serve my-express-apiExpected 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+Cto stop.
- Open your browser or use a tool like Postman/Insomnia and navigate to
Exercise 3.1.3: Update the Dependency Graph
- Run
nx graphagain:nx graph- Observe how the graph has expanded! You now have three distinct application projects:
my-react-app,my-angular-app, andmy-express-api. They are currently independent.
- Observe how the graph has expanded! You now have three distinct application projects:
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.
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 withinlibs/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 necessarypackage.jsonfor 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 likeimport { Button } from '@my-nx-org/shared/ui-components';instead of long relative paths. Nx automatically configurestsconfig.base.json.--tags="scope:shared,type:ui": We’re adding tags. These are crucial for module boundary enforcement later (see Section 4).scope:sharedmeans it’s available to all,type:uicategorizes it.
Expected Output (files created in
libs/shared/ui/ui-components, updates tonx.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-componentsAdd a simple
Buttoncomponent 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 ourui-componentslibrary.--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 buttonEdit
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;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.
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 inlibs/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-modelsDefine a simple
Productinterface and a utility function inlibs/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); }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
Open
apps/my-react-app/src/app/app.tsxand 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.
- Crucially: Notice the clean import:
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-componentslibrary, right inside your React application! Click them to see their effects. - Press
Ctrl+Cto stop.
- Go to
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.
Open
apps/my-express-api/src/main.tsand 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);Serve
my-express-api:nx serve my-express-apiTest the API:
GET Products: Open
http://localhost:3000/api/productsin 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
formattedPriceis 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/productsagain, you’ll see your new product.- Method:
Press
Ctrl+Cto stop the API server.
Exercise 3.3.3: Use the Data Models Library in my-angular-app
Open
apps/my-angular-app/src/app/app.component.tsand 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}`); } }Open
apps/my-angular-app/src/app/app.component.htmland 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>Open
apps/my-angular-app/src/app/app.component.scssand 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; } } }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 tohttp://localhost:3000instead.”
- This tells Angular: “If you get a request to
Update
apps/my-angular-app/project.jsonto use this proxy configuration. Find theservetarget and addproxyConfigto itsoptions.Expected
apps/my-angular-app/project.json(partial, locateservetarget):// 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": { /* ... */ } } }
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
Productinterface andformatCurrencyutility.
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:
- Nx compares your current code state to a base (e.g., your
mainbranch or a previous commit). - It uses the dependency graph (remember
nx graph?) to intelligently figure out which files have changed and which projects consume those changes. - It then executes the specified target only for those “affected” projects.
Exercise 3.4.1: See affected in action
Stop all running servers (Ctrl+C in both terminal tabs).
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.
- Open
View affected projects on the graph:
nx affected:graphExpected Output (browser opens, graph highlights):
- In the interactive graph, you should see
data-modelshighlighted in a distinct color. More importantly, you’ll seemy-express-api,my-react-app, andmy-angular-appalso highlighted! - Why? Because all three applications import from
data-models, any change todata-modelsaffects them, even if they don’t directly use the newOrderinterface yet. Nx is smart enough to know this dependency.
- In the interactive graph, you should see
Run tests for affected projects:
nx affected --target=testExpected Output (only tests for
data-models,my-express-api,my-react-app,my-angular-apprun):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!
Build affected projects:
nx affected --target=buildExpected 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.Commit your changes:
git add . git commit -m "feat: Add Order interface to shared data models"Run
nx affected:graphagain: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
mainbranch. This is the beauty of caching!
- 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
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.
Open
nx.jsonin your workspace.Add
enforceModuleBoundariesrules under thepluginsarray (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 thesourceTagproject.
Interpretation of our rules:
type:app(e.g.,my-react-app,my-angular-app,my-express-api): Can depend ontype:ui,type:data, or anyscope:sharedlibrary. This is generally flexible for applications.type:ui(e.g.,ui-components): Can only depend on libraries taggedtype:util(if we had one),type:data(likedata-models), or otherscope:sharedlibraries. It cannot depend on anappor a feature-specific library.type:data(e.g.,data-models): Can only depend ontype:utilorscope: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.
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.
- Open
Run the lint command for the affected project:
nx lint ui-componentsExpected 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.
Remove the bad import from
libs/shared/ui/ui-components/src/lib/button/button.tsxand 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-componentsagain, and it should now pass.
- Run
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-componentswith--publishablewhen generating it, which created apackage.jsoninside its folder (libs/shared/ui/ui-components/package.json).
- View release information:Expected Output (shows what versions would be bumped):
nx release version --dry-runNX 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-runis crucial! It shows you what would happen without actually making changes. - It detects
my-react-appetc. are apps, andui-components,data-modelsare libraries ready for potential publishing. It suggests version bumps for all projects. nx releaseis highly configurable (e.g., independent versioning, custom changelog formats). This is a great topic for advanced learning.
- The
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.
- To connect to Nx Cloud (if you decide to later):Follow the prompts. It’s free for open source and small teams.
nx connect
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 (
enforceModuleBoundariesinnx.json) rigorously. - Consistent Tooling: Leverage Nx’s generators and
targetDefaultsto 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. Usenx affected --target=testin your CI pipeline to only run relevant tests. nx graphOften: Regularly visualize your dependency graph (nx graph) to catch unintended dependencies early.- Regular Nx Updates: Keep Nx up-to-date using
nx migrate latestto 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:
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)
- React Admin Panel:
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.tsto export:// libs/shared/ecom-domain/src/index.ts export * from './lib/ecom-domain';
- Edit
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
Buttoncomponent inshared-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; }
- Edit
Generate
Cardcomponent inshared-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; }
- Edit
Implement the Node.js API (
api-server):Edit
apps/api-server/src/main.tsto useProduct&Orderfrom@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.)
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.jsonunder theservetarget 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 }, // ... } } }
- Update
Run the React Admin Panel:
nx serve admin-panel(Keep this running in another terminal tab.)
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.jsonunder theservetarget 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 }, // ... } } }
- Update
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:
Open two browser tabs:
- One for your React Admin Panel (likely
http://localhost:4200) - One for your Angular Storefront (likely
http://localhost:4201or similar, check terminal output for exact port)
- One for your React Admin Panel (likely
Interact with the Storefront:
- Browse the products.
- Add some products to your cart.
- Click “Checkout” – this will send a
POST /api/ordersrequest to your Node.js API.
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
ProductandOrderinterfaces, along with theformatPriceutility, are defined once inlibs/shared/ecom-domainand 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-panelis usingButtonandCardcomponents fromlibs/shared/shared-ui. This centralizes your design system. - Unified Development Experience: You use
nx serveto run Angular, React, and Node.js applications, abstracting away framework-specific commands. - Dependency Visualization: Run
nx graphagain. You’ll see a rich graph showing how all your apps depend on@ecom/domain, and howadmin-panelalso depends on@ecom/shared-ui. - Optimized Workflows: Imagine if you needed to add a new field to
Product. You’d modify it inlibs/shared/ecom-domain. Then, runningnx affected --target=testwould only testapi-server,admin-panel, andstorefront-app(andecom-domainitself) 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.
Recommended Online Courses/Tutorials:
- 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:
- Nx Documentation (Official Website): https://nx.dev/
- Getting Started with Nx: https://nx.dev/getting-started/intro - Review this again as you learn more.
- Nx Reference: https://nx.dev/reference - Detailed API documentation for all commands and configurations.
- Nx Cloud Documentation: https://nx.app/ - For understanding remote caching and distributed task execution.
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
nxornx-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)