Skip to main content

Nx Workspace: Creating Your First Monorepo

Now that you understand why monorepos are powerful, let's build one together! Today, we're going to create your first Nx workspace from scratch. By the end of this guide, you'll have a working monorepo with a React frontend and Node.js backend that share TypeScript types—and you'll understand exactly how it all fits together.

Think of this as setting up your new development home. We're not just running commands blindly—we're going to explore each file, understand what Nx creates for us, and discover how the pieces connect. Let's start building!

Quick Reference

Create new workspace:

npx create-nx-workspace@latest my-workspace

Generate new application:

nx generate @nx/react:app my-app
nx generate @nx/node:app api

Essential commands:

nx serve my-app              # Start development server
nx build my-app # Build for production
nx test my-app # Run tests
nx lint my-app # Run linting
nx graph # Visualize dependencies

Common patterns:

  • One workspace per team/project group
  • Apps in /apps directory
  • Shared libraries in /libs directory
  • Configuration at root level

Gotchas:

  • ⚠️ Always run nx commands from workspace root
  • ⚠️ Node.js 18+ required
  • ⚠️ First run may be slow (downloads packages)
  • ⚠️ Watch for port conflicts (default: 4200)

What You Need to Know First

To follow along with this guide, you should have:

Required:

  • Terminal basics: How to navigate directories and run commands (see our Terminal guide)
  • npm/npx knowledge: What these tools do (see our npx guide)
  • Node.js installed: Version 18 or higher (download here)
  • Basic JavaScript/TypeScript: Variables, functions, imports

Helpful context:

Tools you'll need:

  • Code editor (VS Code recommended)
  • Terminal/Command Prompt
  • Git (optional but recommended)

What We'll Cover in This Article

By the end of this guide, you'll understand:

  • How to install Nx and create a workspace
  • What each generated file and folder does
  • How to generate React and Node.js applications
  • How to run and build your applications
  • How to visualize your project dependencies
  • Essential Nx commands for daily development
  • How Nx configuration files work

What We'll Explain Along the Way

Don't worry if these are new to you—we'll explain them step by step:

  • Workspace structure and conventions
  • Package managers (npm, pnpm, yarn)
  • Nx generators and their options
  • Build targets and executors
  • Caching mechanisms
  • Project configuration files

Installing Nx: Your First Step

Let's start our journey by creating your first Nx workspace. We'll go through this step by step, explaining what's happening at each stage.

Understanding npx create-nx-workspace

Before we run any commands, let's understand what we're doing. The command npx create-nx-workspace does several things:

  1. Downloads the latest Nx workspace creator
  2. Asks you questions about your project
  3. Generates a complete workspace structure
  4. Installs all necessary dependencies
  5. Initializes Git repository (optional)

Think of it like this: You're not just creating a folder—you're setting up an entire development environment with tooling, configuration, and structure ready to go.

Creating Your First Workspace

Let's create a workspace called "shophub" (our e-commerce example). Open your terminal and follow along:

# Navigate to where you want to create your workspace
cd ~/projects

# Create the workspace
npx create-nx-workspace@latest shophub

What happens next: Nx will ask you several questions. Let's walk through each one and understand what they mean.

Question 1: Which stack do you want to use?

? Which stack do you want to use? …
None: Configures a TypeScript/JavaScript project with minimal structure.
React: Configures a React application with modern tooling.
Vue: Configures a Vue application with modern tooling.
Angular: Configures an Angular application with modern tooling.
Node: Configures a Node.js application.

What this means: Nx is asking what type of first application you want to create. This doesn't lock you in—you can add other types later!

For our example, choose: React

Why? We're building a web application, so React is perfect. We'll add a Node.js backend later.

Question 2: Application name

? Application name › shophub-web

What this means: The name of your first application. This will become a folder in apps/shophub-web.

Naming convention:

  • Use kebab-case (lowercase with hyphens)
  • Be descriptive: web, mobile-app, admin-dashboard
  • Avoid generic names: app, project, my-app

For our example, use: shophub-web

Question 3: Which bundler would you like to use?

? Which bundler would you like to use? …
Vite (Recommended - Fast, modern)
Webpack (Traditional, more configurable)
Rspack (Rust-based, experimental)

What this means: The tool that bundles your code for the browser.

Think of it like this: A bundler is like a packer who takes all your code files, images, and styles, and packages them efficiently for delivery to users.

For our example, choose: Vite (it's faster and modern)

Question 4: Test runner

? Test runner to use for end to end (E2E) tests …
Playwright (Recommended)
Cypress
None

What this means: Tool for automated browser testing.

For our example, choose: Playwright (it's modern and reliable)

Question 5: Default stylesheet format

? Default stylesheet format …
CSS
SCSS
Styled Components
Emotion
Tailwind CSS

What this means: How you want to style your React components.

For our example, choose: CSS (keeps it simple for learning)

Question 6: Enable distributed caching

? Enable distributed caching to make your CI faster? (y/N)

What this means: Connect to Nx Cloud for shared build caching across your team.

For our example, choose: N (we'll explore this in advanced articles)

Watching Nx Work

After answering the questions, Nx springs into action:

Creating your v19.0.0 workspace.

✔ Installing dependencies with npm
✔ Nx has successfully created the workspace: shophub

What just happened:

  1. Created workspace folder - shophub/ directory
  2. Generated project structure - Apps, configs, tooling
  3. Installed dependencies - Downloaded ~500MB of packages
  4. Configured tooling - Set up TypeScript, ESLint, testing
  5. Initialized Git - Created .git repository

Time: Usually 2-5 minutes depending on internet speed.

Let's see what Nx created for us!


Exploring Your New Workspace

Now let's explore what Nx generated. Navigate into your workspace and look around:

cd shophub
ls -la

You'll see something like this:

shophub/
├── apps/ ← Your applications live here
│ ├── shophub-web/ ← The React app we created
│ └── shophub-web-e2e/ ← E2E tests for React app
├── libs/ ← Shared libraries (empty for now)
├── node_modules/ ← Installed dependencies
├── tools/ ← Custom scripts and generators
├── .vscode/ ← VS Code settings
├── nx.json ← Nx workspace configuration
├── package.json ← Workspace dependencies
├── tsconfig.base.json ← TypeScript base config
├── .gitignore ← Git ignore rules
└── README.md ← Workspace documentation

Let's explore each part and understand what it does.

The /apps Directory: Your Applications

cd apps
ls -la
apps/
├── shophub-web/
│ ├── src/ ← Application source code
│ │ ├── app/ ← React components
│ │ │ ├── app.tsx ← Root component
│ │ │ └── app.module.css
│ │ ├── main.tsx ← Application entry point
│ │ └── index.html ← HTML template
│ ├── public/ ← Static assets
│ ├── project.json ← Project configuration
│ ├── tsconfig.json ← TypeScript config
│ ├── vite.config.ts ← Vite bundler config
│ └── .eslintrc.json ← Linting rules
└── shophub-web-e2e/
├── src/
│ └── example.spec.ts ← E2E test example
└── playwright.config.ts ← Test configuration

What's an app? An application is something you deploy—a website, API server, mobile app, or backend service. Apps are the "end products" of your workspace.

Key files explained:

1. main.tsx - The entry point

// apps/shophub-web/src/main.tsx
import { StrictMode } from "react";
import * as ReactDOM from "react-dom/client";
import App from "./app/app";

const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);

root.render(
<StrictMode>
<App />
</StrictMode>
);

This is where your application starts. Think of it as the "main()" function of your React app.

2. app.tsx - Your root component

// apps/shophub-web/src/app/app.tsx
export function App() {
return (
<div>
<h1>Welcome to ShopHub!</h1>
</div>
);
}

export default App;

This is your first React component. Everything else builds from here.

3. project.json - Project configuration

{
"name": "shophub-web",
"sourceRoot": "apps/shophub-web/src",
"projectType": "application",
"tags": [],
"targets": {
"build": {
"executor": "@nx/vite:build",
"options": {
"outputPath": "dist/apps/shophub-web"
}
},
"serve": {
"executor": "@nx/vite:dev-server",
"options": {
"buildTarget": "shophub-web:build",
"port": 4200
}
},
"test": {
"executor": "@nx/vite:test"
}
}
}

This tells Nx how to build, serve, and test your app. We'll dive deeper into this soon.

The /libs Directory: Shared Code

cd ../libs
ls -la

Right now, this is empty! But this is where the magic happens. Let's understand what will go here:

libs/                         ← Future shared libraries
├── shared/ ← Code shared by all apps
│ ├── ui/ ← Reusable components
│ ├── util/ ← Utility functions
│ └── types/ ← TypeScript types
├── feature/ ← Feature modules
│ ├── auth/ ← Authentication logic
│ └── products/ ← Product catalog
└── data-access/ ← API communication
└── api-client/ ← HTTP client

What's a library? A library is reusable code that multiple apps can import. Libraries are NOT deployed—they're building blocks for apps.

Library types (we'll create these later):

TypePurposeExample
uiReusable componentsButton, Card, Modal
utilPure functionsformatDate, validateEmail
typesTypeScript definitionsUser, Product, Order
featureFeature modulesAuth system, Product catalog
data-accessAPI communicationHTTP clients, API wrappers

Root Configuration Files

Let's understand the important files at the workspace root:

1. package.json - Workspace Dependencies

{
"name": "@shophub/source",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"start": "nx serve",
"build": "nx build",
"test": "nx test"
},
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@nx/vite": "19.0.0",
"@nx/react": "19.0.0",
"@nx/eslint": "19.0.0",
"typescript": "5.4.5",
"vite": "5.2.8"
}
}

Key insight: Notice there's only ONE package.json for the entire workspace. All apps and libraries share these dependencies!

Think of it like this: In a polyrepo, you'd have separate package.json files with possibly different versions of React. In a monorepo, everyone uses the same versions—consistency guaranteed!

2. nx.json - Nx Configuration

{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"build": {
"cache": true,
"dependsOn": ["^build"]
},
"test": {
"cache": true
}
},
"defaultBase": "main"
}

What this controls:

  • Caching: Which commands cache their results
  • Dependencies: Build order for dependent projects
  • Defaults: Default behavior for all projects

The key setting: "cache": true means Nx remembers build results. If nothing changed, it uses the cached version instantly!

3. tsconfig.base.json - TypeScript Paths

{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"jsx": "react-jsx",
"moduleResolution": "bundler",
"paths": {
"@shophub/shared-types": ["libs/shared/types/src/index.ts"]
}
}
}

This is crucial! The paths section lets you use clean imports:

// Instead of this ugly relative import:
import { User } from "../../../libs/shared/types/src/user";

// You can write this beautiful absolute import:
import { User } from "@shophub/shared-types";

Nx automatically manages these paths when you create libraries!


Running Your First Application

Let's see your React app in action! This is where things get exciting.

Starting the Development Server

# Make sure you're in workspace root
cd ~/projects/shophub

# Start the development server
nx serve shophub-web

What happens:

> nx run shophub-web:serve

NX Web Development Server is listening at http://localhost:4200/

✔ Vite ready in 1234 ms
✔ Built in 567 ms

Behind the scenes:

  1. Nx reads apps/shophub-web/project.json
  2. Finds the serve target configuration
  3. Runs Vite dev server with specified options
  4. Compiles your TypeScript to JavaScript
  5. Bundles React components
  6. Starts server on port 4200
  7. Watches for file changes

Viewing Your Application

Open your browser and navigate to:

http://localhost:4200

You should see:

Welcome to ShopHub!

Congratulations! Your first Nx application is running!

Hot Reload: Watch the Magic

Let's see hot reload in action. Keep the server running and open your app file:

# In a new terminal
code apps/shophub-web/src/app/app.tsx

Change the content:

// apps/shophub-web/src/app/app.tsx
export function App() {
return (
<div>
<h1>Welcome to ShopHub! 🛍️</h1>
<p>Your one-stop shop for everything</p>
</div>
);
}

export default App;

Save the file and watch your browser—it updates instantly without refreshing! This is hot module replacement (HMR) at work.

What just happened:

  1. You saved the file
  2. Vite detected the change
  3. Vite recompiled just that one component
  4. Vite sent the update to browser via WebSocket
  5. React updated the component without full page reload
  6. Your app state is preserved!

Time from save to browser update: Usually under 100ms!

Stopping the Server

Press Ctrl+C in the terminal to stop the development server.


Adding a Backend: Node.js API

Now let's add a Node.js backend to our workspace. This is where monorepos really shine—we'll see how easy it is to add a completely different type of application.

Generating a Node.js Application

# Generate a Node.js app
nx generate @nx/node:application api

# Nx will ask some questions:

Question 1: Which framework would you like to use?

? Which framework would you like to use? …
Express (Popular, simple)
Fastify (Fast, modern)
Koa (Minimalist)
NestJS (Angular-style for Node)
None (Plain Node.js)

Choose: Express (most popular, great for learning)

Question 2: Test runner

? Test runner to use for unit tests …
Jest
None

Choose: Jest (industry standard)

Question 3: Enable distributed caching?

? Enable distributed caching? (y/N)

Choose: N

What Did Nx Just Create?

ls -la apps/api
apps/api/
├── src/
│ ├── main.ts ← Server entry point
│ └── app/
│ └── routes.ts ← API routes
├── project.json ← Project configuration
├── tsconfig.json ← TypeScript config
├── tsconfig.app.json ← App-specific TS config
├── jest.config.ts ← Test configuration
└── .eslintrc.json ← Linting rules

Let's look at the generated server code:

// apps/api/src/main.ts
import express from "express";

const app = express();

app.get("/api", (req, res) => {
res.send({ message: "Welcome to api!" });
});

const port = process.env.PORT || 3333;
const server = app.listen(port, () => {
console.log(`Listening at http://localhost:${port}/api`);
});

server.on("error", console.error);

Simple and clean! Nx created a basic Express server ready to extend.

Running Frontend and Backend Together

Here's where Nx's power shows. Let's run both apps simultaneously:

# Option 1: Run in separate terminals

# Terminal 1:
nx serve shophub-web

# Terminal 2:
nx serve api

Now you have:

  • Frontend at http://localhost:4200
  • Backend at http://localhost:3333

Option 2: Run both with one command (we'll set this up shortly)

Your First Full-Stack Monorepo!

Let's verify both are working:

# Test the API
curl http://localhost:3333/api

# Response:
{"message":"Welcome to api!"}

Success! You now have:

  • ✅ React frontend
  • ✅ Node.js backend
  • ✅ Both in one workspace
  • ✅ Shared tooling and configuration
  • ✅ Easy to run and develop

Creating Your First Shared Library

Now let's see the real power of monorepos: sharing code between your frontend and backend. We'll create shared TypeScript types that both apps can use.

Generating a Shared Library

# Generate a library for shared types
nx generate @nx/js:library shared-types --directory=shared

# Nx asks:
? Which unit test runner would you like to use? (Use arrow keys)
jest
vitest
none

Choose: jest

What Did Nx Create?

ls -la libs/shared/shared-types
libs/shared/shared-types/
├── src/
│ ├── index.ts ← Main export file
│ └── lib/
│ └── shared-shared-types.ts ← Generated example
├── project.json ← Project configuration
├── tsconfig.json ← TypeScript config
├── tsconfig.lib.json ← Library-specific TS config
├── jest.config.ts ← Test configuration
└── .eslintrc.json ← Linting rules

Notice: Nx automatically updated tsconfig.base.json with the import path!

// tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@shophub/shared-types": ["libs/shared/shared-types/src/index.ts"]
}
}
}

Creating Shared Types

Let's create some useful types that both frontend and backend can use:

// libs/shared/shared-types/src/lib/product.ts

/**
* Product entity representing an item in our catalog
*/
export interface Product {
id: string;
name: string;
description: string;
price: number;
imageUrl: string;
category: string;
inStock: boolean;
createdAt: Date;
}

/**
* API response for product list
*/
export interface ProductListResponse {
products: Product[];
total: number;
page: number;
pageSize: number;
}

/**
* Request to create a new product
*/
export interface CreateProductRequest {
name: string;
description: string;
price: number;
imageUrl: string;
category: string;
}
// libs/shared/shared-types/src/lib/user.ts

/**
* User entity
*/
export interface User {
id: string;
email: string;
firstName: string;
lastName: string;
role: UserRole;
createdAt: Date;
}

/**
* User roles in the system
*/
export enum UserRole {
Customer = "CUSTOMER",
Admin = "ADMIN",
Moderator = "MODERATOR",
}

/**
* API response for authentication
*/
export interface AuthResponse {
user: User;
token: string;
expiresAt: Date;
}

Exporting Your Types

Now update the index file to export everything:

// libs/shared/shared-types/src/index.ts

// Product types
export * from "./lib/product";

// User types
export * from "./lib/user";

This is your public API. Only what you export here is accessible to other projects.

Using Shared Types in Backend

Let's update the API to use our shared types:

// apps/api/src/main.ts
import express from "express";
import { Product, ProductListResponse } from "@shophub/shared-types";

const app = express();
app.use(express.json());

// Mock database
const products: Product[] = [
{
id: "1",
name: "Laptop",
description: "High-performance laptop",
price: 999.99,
imageUrl: "/images/laptop.jpg",
category: "Electronics",
inStock: true,
createdAt: new Date(),
},
{
id: "2",
name: "Headphones",
description: "Noise-cancelling headphones",
price: 299.99,
imageUrl: "/images/headphones.jpg",
category: "Electronics",
inStock: true,
createdAt: new Date(),
},
];

// API endpoint with typed response
app.get("/api/products", (req, res) => {
const response: ProductListResponse = {
products: products,
total: products.length,
page: 1,
pageSize: 10,
};

res.json(response);
});

const port = process.env.PORT || 3333;
const server = app.listen(port, () => {
console.log(`Listening at http://localhost:${port}/api`);
});

server.on("error", console.error);

Notice: TypeScript now knows exactly what shape your API responses have!

Using Shared Types in Frontend

Now let's use the same types in our React app:

// apps/shophub-web/src/app/app.tsx
import { useEffect, useState } from "react";
import { Product, ProductListResponse } from "@shophub/shared-types";
import styles from "./app.module.css";

export function App() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
// Fetch products from API
fetch("http://localhost:3333/api/products")
.then((res) => res.json() as Promise<ProductListResponse>)
.then((data) => {
setProducts(data.products);
setLoading(false);
})
.catch((err) => {
console.error("Failed to fetch products:", err);
setLoading(false);
});
}, []);

if (loading) {
return <div className={styles.app}>Loading products...</div>;
}

return (
<div className={styles.app}>
<h1>Welcome to ShopHub! 🛍️</h1>
<h2>Our Products</h2>

<div className={styles.productGrid}>
{products.map((product) => (
<div key={product.id} className={styles.productCard}>
<h3>{product.name}</h3>
<p>{product.description}</p>
<p className={styles.price}>${product.price.toFixed(2)}</p>
<p className={styles.category}>{product.category}</p>
{product.inStock ? (
<span className={styles.inStock}>In Stock</span>
) : (
<span className={styles.outOfStock}>Out of Stock</span>
)}
</div>
))}
</div>
</div>
);
}

export default App;

Add some basic styling:

/* apps/shophub-web/src/app/app.module.css */
.app {
padding: 2rem;
font-family: sans-serif;
}

.productGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}

.productCard {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.productCard h3 {
margin-top: 0;
color: #333;
}

.price {
font-size: 1.5rem;
font-weight: bold;
color: #0066cc;
margin: 0.5rem 0;
}

.category {
color: #666;
font-size: 0.9rem;
margin: 0.5rem 0;
}

.inStock {
display: inline-block;
background: #4caf50;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.85rem;
}

.outOfStock {
display: inline-block;
background: #f44336;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.85rem;
}

Testing Your Full-Stack Application

Now start both apps:

# Terminal 1: Start API
nx serve api

# Terminal 2: Start frontend
nx serve shophub-web

Open http://localhost:4200 and you should see:

  • Product list loaded from API
  • Typed data throughout
  • Beautiful product cards

The magic: Both frontend and backend use the SAME type definitions. If you change Product interface, TypeScript will immediately tell you everywhere that needs updating!


Essential Nx Commands

Let's explore the commands you'll use every day. We'll try each one so you understand what it does.

1. Building for Production

# Build a specific project
nx build shophub-web

What happens:

> nx run shophub-web:build:production

✔ Building entry: apps/shophub-web/src/main.tsx
✔ Computed index.html
✔ Built in 2.5s

Output: dist/apps/shophub-web
- index.html
- assets/
- main.js (minified)
- vendor.js (minified)

The dist/ folder contains production-ready files you can deploy!

# Build multiple projects
nx build shophub-web
nx build api

# Or build everything
nx run-many --target=build --all

2. Running Tests

# Test a specific project
nx test shared-types

# Output:
PASS libs/shared/shared-types/src/lib/shared-shared-types.spec.ts
✓ should work (3 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
# Test all projects
nx run-many --target=test --all

# Test with coverage
nx test shared-types --coverage

3. Linting Your Code

# Lint a specific project
nx lint shophub-web

# Lint everything
nx run-many --target=lint --all

# Auto-fix issues
nx lint shophub-web --fix

4. Visualizing Dependencies

This is one of Nx's most powerful features. Let's see your project structure visually!

# Generate dependency graph
nx graph

What happens: Nx opens a browser with an interactive graph showing how your projects relate.

What you can do:

  • Click nodes to highlight dependencies
  • See which projects depend on what
  • Identify circular dependencies (red lines)
  • Plan refactoring

Pro tip: Run this regularly to understand your architecture!

5. Affected Commands: The Smart Way

Remember how we talked about only building what changed? Here's how:

# See what's affected by your changes
nx affected:graph

# Build only affected projects
nx affected:build

# Test only affected projects
nx affected:test

# Lint only affected projects
nx affected:lint

Example scenario:

# You changed shared-types library
# What's affected?
nx affected:graph

# Shows:
# - shared-types (you changed it)
# - shophub-web (uses shared-types)
# - api (uses shared-types)

# NOT affected:
# - shophub-web-e2e (tests don't use types directly)

Time savings:

Without affected commands:
- Build all 3 projects: 6 minutes
- Test all 3 projects: 4 minutes
- Total: 10 minutes

With affected commands:
- Build 2 affected projects: 4 minutes
- Test 2 affected projects: 2 minutes
- Total: 6 minutes (40% faster!)

6. Caching: See the Speed

Nx caches everything by default. Let's see it in action:

# First build (no cache)
nx build shophub-web
# Output: Built in 3.2s

# Build again (with cache)
nx build shophub-web
# Output:
# Nx read the output from the cache instead of running the command for 1 out of 1 tasks.
# ✔ Built in 0.1s (cached)

32x faster! The second build is instant because nothing changed.

Clear cache if needed:

nx reset

7. Running Multiple Commands

# Run same command on multiple projects
nx run-many --target=build --projects=shophub-web,api

# Run on all projects
nx run-many --target=test --all

# Run in parallel (faster!)
nx run-many --target=build --all --parallel=3

8. Generating Code

We've already used generators to create apps and libraries. Here are more examples:

# Generate a React component
nx generate @nx/react:component Button --project=shophub-web

# Generate a library
nx generate @nx/js:library feature-auth --directory=feature

# Generate a service
nx generate @nx/node:service ProductService --project=api

# See all available generators
nx list @nx/react
nx list @nx/node

Understanding Project Configuration

Let's dive deeper into how Nx configures projects. This is crucial for customizing your workspace.

The project.json File

Every project has a project.json file. Let's examine it in detail:

// apps/shophub-web/project.json
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "shophub-web",
"projectType": "application",
"sourceRoot": "apps/shophub-web/src",
"tags": [],
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/shophub-web",
"index": "apps/shophub-web/index.html",
"main": "apps/shophub-web/src/main.tsx",
"tsConfig": "apps/shophub-web/tsconfig.app.json"
},
"configurations": {
"development": {
"mode": "development"
},
"production": {
"mode": "production"
}
},
"defaultConfiguration": "production"
},
"serve": {
"executor": "@nx/vite:dev-server",
"options": {
"buildTarget": "shophub-web:build",
"port": 4200
},
"configurations": {
"development": {
"buildTarget": "shophub-web:build:development",
"hmr": true
},
"production": {
"buildTarget": "shophub-web:build:production"
}
},
"defaultConfiguration": "development"
},
"test": {
"executor": "@nx/vite:test",
"options": {
"passWithNoTests": true,
"reportsDirectory": "../../coverage/apps/shophub-web"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

Let's break this down:

Basic Information

{
"name": "shophub-web", // Project identifier
"projectType": "application", // "application" or "library"
"sourceRoot": "apps/shophub-web/src", // Where source code lives
"tags": [] // Labels for dependency rules
}

Project types:

  • application: Something you deploy (web app, API, mobile app)
  • library: Shared code used by applications

Targets: What You Can Do

{
"targets": {
"build": { ... }, // How to build for production
"serve": { ... }, // How to run development server
"test": { ... }, // How to run tests
"lint": { ... } // How to check code quality
}
}

Each target defines a task you can run with nx <target> <project>.

Executors: How Tasks Run

{
"build": {
"executor": "@nx/vite:build", // Nx plugin that runs the task
"options": { ... } // Configuration for the executor
}
}

Executors are like recipes. They know how to perform specific tasks:

  • @nx/vite:build knows how to build with Vite
  • @nx/webpack:webpack knows how to build with Webpack
  • @nx/jest:jest knows how to run Jest tests

Configurations: Different Modes

{
"build": {
"configurations": {
"development": {
"mode": "development"
},
"production": {
"mode": "production"
}
},
"defaultConfiguration": "production"
}
}

Run with specific configuration:

nx build shophub-web --configuration=development
nx build shophub-web --configuration=production
nx build shophub-web # Uses default (production)

Customizing Build Configuration

Let's add a staging configuration:

// apps/shophub-web/project.json
{
"targets": {
"build": {
"configurations": {
"development": {
"mode": "development"
},
"staging": {
"mode": "production",
"sourcemap": true, // Keep sourcemaps for debugging
"optimization": true
},
"production": {
"mode": "production",
"sourcemap": false, // No sourcemaps in prod
"optimization": true
}
}
}
}
}

Now you can build for staging:

nx build shophub-web --configuration=staging

Environment Variables

Create environment-specific configs:

// apps/shophub-web/src/environments/environment.ts
export const environment = {
production: false,
apiUrl: "http://localhost:3333/api",
};
// apps/shophub-web/src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: "https://api.shophub.com",
};

Use in your app:

// apps/shophub-web/src/app/app.tsx
import { environment } from "../environments/environment";

export function App() {
const [products, setProducts] = useState<Product[]>([]);

useEffect(() => {
// Uses correct API URL for environment
fetch(`${environment.apiUrl}/products`)
.then((res) => res.json())
.then((data) => setProducts(data.products));
}, []);

// ... rest of component
}

Workspace Structure Best Practices

Now that you understand the basics, let's talk about how to organize your workspace as it grows.

Organizing Apps

apps/
├── customer/ ← Customer-facing apps
│ ├── web/ ← Main website
│ ├── mobile/ ← Mobile app
│ └── web-e2e/ ← E2E tests
├── admin/ ← Internal admin tools
│ ├── dashboard/ ← Admin dashboard
│ └── dashboard-e2e/ ← E2E tests
└── api/ ← Backend services
├── products/ ← Product service
├── auth/ ← Authentication service
└── payments/ ← Payment service

Grouping strategy:

  • By audience (customer, admin, internal)
  • By platform (web, mobile, desktop)
  • By service (if microservices)

Organizing Libraries

libs/
├── shared/ ← Used by all apps
│ ├── ui/ ← Common UI components
│ │ ├── button/
│ │ ├── card/
│ │ └── modal/
│ ├── util/ ← Utility functions
│ │ ├── formatting/
│ │ ├── validation/
│ │ └── date/
│ └── types/ ← TypeScript definitions
│ ├── user.ts
│ ├── product.ts
│ └── order.ts
├── customer/ ← Customer-specific features
│ ├── feature-products/ ← Product catalog
│ ├── feature-cart/ ← Shopping cart
│ └── feature-checkout/ ← Checkout flow
├── admin/ ← Admin-specific features
│ ├── feature-users/ ← User management
│ └── feature-analytics/ ← Analytics dashboard
└── data-access/ ← API communication
├── api-client/ ← HTTP client
└── websocket/ ← WebSocket client

Library naming convention:

PatternExamplePurpose
shared-*shared-uiUsed by all apps
feature-*feature-authFeature modules
ui-*ui-buttonsUI components
util-*util-formattingUtility functions
data-access-*data-access-productsAPI clients

Project Tags and Constraints

Use tags to enforce architectural boundaries:

// apps/customer/web/project.json
{
"tags": ["scope:customer", "type:app"]
}

// libs/customer/feature-cart/project.json
{
"tags": ["scope:customer", "type:feature"]
}

// libs/shared/ui/project.json
{
"tags": ["scope:shared", "type:ui"]
}

Enforce rules in .eslintrc.json:

{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:customer",
"onlyDependOnLibsWithTags": ["scope:customer", "scope:shared"]
},
{
"sourceTag": "scope:admin",
"onlyDependOnLibsWithTags": ["scope:admin", "scope:shared"]
},
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
}
]
}
]
}
}

What this enforces:

✅ Customer apps can use customer libraries and shared libraries ❌ Customer apps CANNOT use admin libraries ✅ Apps can use feature, ui, and util libraries ❌ Apps CANNOT import from other apps

Try to break the rule:

// apps/customer/web/src/app/app.tsx
import { AdminComponent } from "@shophub/admin-dashboard"; // ❌ LINT ERROR!
// "Projects tagged with "scope:customer" can only depend on
// libs tagged with "scope:customer" or "scope:shared""

Advanced Configuration

Let's explore some powerful configurations that make development smoother.

Running Multiple Apps Together

Create a convenience script to run frontend and backend together:

// package.json
{
"scripts": {
"dev": "nx run-many --target=serve --projects=shophub-web,api --parallel"
}
}

Now start everything with one command:

npm run dev

Both apps start in parallel! 🚀

Custom Workspace Generators

Create templates for common patterns in your workspace:

# Generate a workspace generator
nx generate @nx/plugin:generator feature-library

This creates a custom generator that follows your team's patterns:

// tools/generators/feature-library/index.ts
import { Tree, formatFiles, generateFiles } from "@nx/devkit";

export default async function (tree: Tree, options: any) {
// Your custom logic to generate feature libraries
// Following your team's specific structure
generateFiles(
tree,
path.join(__dirname, "files"),
options.directory,
options
);

await formatFiles(tree);
}

Use your custom generator:

nx workspace-generator feature-library --name=products

Configuring VS Code

Create workspace-specific VS Code settings:

// .vscode/settings.json
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"files.associations": {
"*.css": "tailwindcss"
},
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}

Recommended VS Code extensions:

// .vscode/extensions.json
{
"recommendations": [
"nrwl.angular-console",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}

Team members who open the workspace will be prompted to install these extensions!


Troubleshooting Common Issues

Let's address problems you might encounter and how to solve them.

Issue 1: Port Already in Use

Problem:

nx serve shophub-web
# Error: Port 4200 is already in use

Solution 1: Kill the process using the port

# On macOS/Linux:
lsof -ti:4200 | xargs kill -9

# On Windows:
netstat -ano | findstr :4200
taskkill /PID <PID> /F

Solution 2: Use a different port

nx serve shophub-web --port=4300

Solution 3: Configure default port

// apps/shophub-web/project.json
{
"targets": {
"serve": {
"options": {
"port": 4300 // ← Change default port
}
}
}
}

Issue 2: Module Not Found After Creating Library

Problem:

import { Product } from "@shophub/shared-types";
// Error: Cannot find module '@shophub/shared-types'

Solution 1: Restart TypeScript server

VS Code: Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server"

Solution 2: Check tsconfig.base.json

// tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@shophub/shared-types": ["libs/shared/shared-types/src/index.ts"]
}
}
}

Solution 3: Clear Nx cache

nx reset

Issue 3: Build Fails After Git Pull

Problem:

git pull
nx build shophub-web
# Error: Cannot find module 'some-new-dependency'

Solution: Reinstall dependencies

# Clear everything and start fresh
rm -rf node_modules
npm install

# Or just update
npm install

Prevention: Always run after pulling:

git pull && npm install

Issue 4: Slow Build Times

Problem:

nx build shophub-web
# Takes 5+ minutes

Solution 1: Enable caching (should be default)

// nx.json
{
"targetDefaults": {
"build": {
"cache": true
}
}
}

Solution 2: Build only what changed

nx affected:build  # Instead of building everything

Solution 3: Check for large dependencies

# Analyze bundle size
nx build shophub-web --stats-json

# Use webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/apps/shophub-web/stats.json

Solution 4: Upgrade Node.js

# Check current version
node --version

# Upgrade to latest LTS
# Download from nodejs.org

Issue 5: Hot Reload Not Working

Problem:

nx serve shophub-web
# Changes don't show in browser

Solution 1: Hard refresh browser

Cmd/Ctrl + Shift + R (Chrome/Firefox)

Solution 2: Clear browser cache

Chrome: Cmd/Ctrl + Shift + Delete → Clear cached images and files

Solution 3: Restart dev server

# Stop server (Ctrl+C)
# Clear Nx cache
nx reset
# Start again
nx serve shophub-web

Solution 4: Check file watcher limits (Linux)

# Increase file watcher limit
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Issue 6: TypeScript Errors in IDE but Build Works

Problem:

  • VS Code shows red squiggly lines
  • nx build succeeds
  • Code runs fine

Solution 1: Restart TypeScript

VS Code: Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server"

Solution 2: Check TypeScript version

# Workspace TypeScript
./node_modules/.bin/tsc --version

# VS Code might be using different version
# Make sure VS Code uses workspace TypeScript

Solution 3: Delete TypeScript cache

# macOS/Linux
rm -rf ~/.tsbuildinfo

# Windows
del /s /q %APPDATA%\tsbuildinfo

Performance Optimization Tips

Let's make your workspace blazing fast!

1. Use Affected Commands

# Instead of:
nx run-many --target=test --all

# Use:
nx affected:test

Savings: 50-90% time reduction on CI

2. Enable Parallel Execution

# Build multiple projects in parallel
nx run-many --target=build --all --parallel=3
// nx.json - Set default parallel execution
{
"parallel": 3
}

3. Optimize Dependencies

Only import what you need:

// ❌ Bad: Imports entire library
import _ from "lodash";

// ✅ Good: Imports only what's needed
import { debounce } from "lodash-es";

4. Use Build Cache

// nx.json
{
"targetDefaults": {
"build": {
"cache": true,
"dependsOn": ["^build"]
},
"test": {
"cache": true
},
"lint": {
"cache": true
}
}
}

5. Split Large Libraries

// ❌ Bad: One giant library
libs/shared/everything/

// ✅ Good: Focused libraries
libs/shared/ui/
libs/shared/util/
libs/shared/types/

Why? Smaller libraries = faster incremental builds

6. Use Buildable Libraries

For large workspaces, make libraries buildable:

nx generate @nx/js:library my-lib --buildable

This pre-compiles the library, making dependent app builds faster.


Summary: Key Takeaways

Congratulations! You've built your first Nx monorepo from scratch. Let's recap what you've learned:

Core Concepts

Workspace Creation - Using create-nx-workspace to scaffold a monorepo ✅ Project Types - Applications (deployable) vs Libraries (reusable) ✅ Code Sharing - Creating shared libraries for types, utilities, and components ✅ Project Structure - /apps for applications, /libs for libraries ✅ Configuration - Understanding project.json, nx.json, and tsconfig.base.json

Essential Commands

nx serve <project>              # Development server
nx build <project> # Production build
nx test <project> # Run tests
nx lint <project> # Check code quality
nx graph # Visualize dependencies
nx affected:build # Build only what changed
nx run-many --target=build --all # Build everything

Best Practices

Organize by domain - Group related projects together ✅ Use tags - Enforce architectural boundaries ✅ Share code through libraries - Not through app-to-app imports ✅ Use affected commands - Only build/test what changed ✅ Enable caching - Speed up builds dramatically ✅ Clear import paths - Use @workspace/library-name format

What You Built

You now have:

  • ✅ React frontend application
  • ✅ Node.js backend API
  • ✅ Shared TypeScript types library
  • ✅ Working full-stack application
  • ✅ Hot reload for rapid development
  • ✅ Production build configuration
  • ✅ Testing setup
  • ✅ Linting configuration

📚 Related topics:


Check Your Understanding

Test what you've learned with these hands-on questions.

Quick Quiz

1. What's the difference between an app and a library in Nx?

Show Answer

App (Application):

  • Something you deploy (website, API, mobile app)
  • Lives in /apps directory
  • Has a main entry point
  • Can import from libraries
  • Cannot be imported by other projects

Library:

  • Reusable code shared between apps
  • Lives in /libs directory
  • Exports functions, components, types
  • Cannot be deployed on its own
  • Can be imported by apps and other libraries

Example:

apps/web/           ← App (deploy to Vercel)
apps/api/ ← App (deploy to AWS)
libs/shared-types/ ← Library (used by both apps)

2. Why is this import pattern better?

// Pattern A
import { User } from "@shophub/shared-types";

// Pattern B
import { User } from "../../../libs/shared/shared-types/src/lib/user";
Show Answer

Pattern A is better because:

  1. Cleaner and more readable - Easy to understand what you're importing
  2. Refactor-safe - If you move files, imports don't break
  3. Absolute paths - No counting ../../../ levels
  4. IDE support - Better autocomplete and refactoring
  5. Consistent - Same import from anywhere in workspace

How it works:

// tsconfig.base.json
{
"paths": {
"@shophub/shared-types": ["libs/shared/shared-types/src/index.ts"]
}
}

Nx manages these paths automatically when you create libraries!

3. What command would you use to build only projects affected by your changes?

Show Answer
nx affected:build

How it works:

  1. Nx compares your current code to the base branch (usually main)
  2. Identifies which files changed
  3. Figures out which projects use those files
  4. Builds only those affected projects

Example:

# You changed libs/shared-types/src/product.ts

nx affected:build

# Builds:
# - shared-types (you changed it)
# - shophub-web (imports from shared-types)
# - api (imports from shared-types)

# Skips:
# - shophub-web-e2e (doesn't use shared-types directly)

Time savings:

  • Full build: 10 minutes (all projects)
  • Affected build: 3 minutes (only 3 projects)
  • 70% faster!

4. Where should shared TypeScript types live in your workspace?

Show Answer

Correct location:

libs/shared/types/

Why?

  1. In /libs - Types are reusable code, not deployable apps
  2. In /shared - Used by multiple apps (customer, admin, API)
  3. Separate library - Types change together, should be one unit

Full example:

libs/
└── shared/
├── types/ ← TypeScript interfaces
│ ├── user.ts
│ ├── product.ts
│ └── order.ts
├── ui/ ← React components
└── util/ ← Utility functions

Import from any app:

import { User, Product } from "@shophub/shared-types";

Wrong locations:

❌ apps/web/src/types/        - Should be in libs
❌ libs/web/types/ - Should be in shared
❌ libs/types/ - Should group under shared

Hands-On Exercise

Challenge: Extend your ShopHub workspace

Tasks:

  1. Create a new shared utility library

    nx generate @nx/js:library shared-utils --directory=shared
  2. Add a price formatting function

    // libs/shared/shared-utils/src/lib/format-price.ts
    export function formatPrice(price: number): string {
    return `${price.toFixed(2)}`;
    }
  3. Export it from the library

    // libs/shared/shared-utils/src/index.ts
    export * from "./lib/format-price";
  4. Use it in your frontend

    // apps/shophub-web/src/app/app.tsx
    import { formatPrice } from "@shophub/shared-utils";

    <p className={styles.price}>{formatPrice(product.price)}</p>;
  5. Visualize the new dependency

    nx graph

You should see shophub-web → shared-utils connection!

Bonus challenges:

  • Add a date formatting function
  • Create validation utilities (email, phone number)
  • Add unit tests for your utilities
  • Use the utilities in your API as well

Version Information

Tested with:

  • Nx: v19.0.0+
  • Node.js: v18.x, v20.x, v22.x
  • npm: v9.x, v10.x
  • React: v18.3.1
  • TypeScript: v5.4.5
  • Vite: v5.2.8

Platform support:

  • ✅ macOS (Intel & Apple Silicon)
  • ✅ Windows 10/11
  • ✅ Linux (Ubuntu 20.04+, Fedora, etc.)
  • ✅ WSL2 on Windows

Last updated: November 2024

Note: Nx releases new versions regularly. The concepts in this article apply to all versions, though specific commands or options may vary slightly.