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
/appsdirectory - Shared libraries in
/libsdirectory - Configuration at root level
Gotchas:
- ⚠️ Always run
nxcommands 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:
- Monorepo vs Polyrepo - Understanding why we use monorepos
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:
- Downloads the latest Nx workspace creator
- Asks you questions about your project
- Generates a complete workspace structure
- Installs all necessary dependencies
- 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:
- Created workspace folder -
shophub/directory - Generated project structure - Apps, configs, tooling
- Installed dependencies - Downloaded ~500MB of packages
- Configured tooling - Set up TypeScript, ESLint, testing
- Initialized Git - Created
.gitrepository
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):
| Type | Purpose | Example |
|---|---|---|
| ui | Reusable components | Button, Card, Modal |
| util | Pure functions | formatDate, validateEmail |
| types | TypeScript definitions | User, Product, Order |
| feature | Feature modules | Auth system, Product catalog |
| data-access | API communication | HTTP 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:
- Nx reads
apps/shophub-web/project.json - Finds the
servetarget configuration - Runs Vite dev server with specified options
- Compiles your TypeScript to JavaScript
- Bundles React components
- Starts server on port 4200
- 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:
- You saved the file
- Vite detected the change
- Vite recompiled just that one component
- Vite sent the update to browser via WebSocket
- React updated the component without full page reload
- 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:buildknows how to build with Vite@nx/webpack:webpackknows how to build with Webpack@nx/jest:jestknows 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:
| Pattern | Example | Purpose |
|---|---|---|
shared-* | shared-ui | Used by all apps |
feature-* | feature-auth | Feature modules |
ui-* | ui-buttons | UI components |
util-* | util-formatting | Utility functions |
data-access-* | data-access-products | API 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 buildsucceeds- 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
/appsdirectory - Has a
mainentry point - Can import from libraries
- Cannot be imported by other projects
Library:
- Reusable code shared between apps
- Lives in
/libsdirectory - 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:
- Cleaner and more readable - Easy to understand what you're importing
- Refactor-safe - If you move files, imports don't break
- Absolute paths - No counting
../../../levels - IDE support - Better autocomplete and refactoring
- 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:
- Nx compares your current code to the base branch (usually
main) - Identifies which files changed
- Figures out which projects use those files
- 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?
- In
/libs- Types are reusable code, not deployable apps - In
/shared- Used by multiple apps (customer, admin, API) - 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:
-
Create a new shared utility library
nx generate @nx/js:library shared-utils --directory=shared -
Add a price formatting function
// libs/shared/shared-utils/src/lib/format-price.ts
export function formatPrice(price: number): string {
return `${price.toFixed(2)}`;
} -
Export it from the library
// libs/shared/shared-utils/src/index.ts
export * from "./lib/format-price"; -
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>; -
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.