Skip to main content

Nx Project Structure: Apps and Libraries

Imagine walking into a massive warehouse where everything is randomly thrown into piles—tools mixed with furniture, electronics scattered among clothing, no labels, no order. Finding anything would be a nightmare! Now picture that same warehouse perfectly organized—tools in one section with clear subcategories, electronics grouped by type, each aisle labeled, everything in its logical place. You could find what you need in seconds.

That's exactly the difference between a poorly structured monorepo and a well-organized one. Today, we're going on a journey to discover how to organize your Nx workspace like a master architect. We'll explore the fundamental difference between apps and libraries, learn when to create each type, understand naming conventions that scale from 10 to 1000 projects, and build an intuitive structure that makes your codebase a joy to navigate.

Think of this as learning the grammar of monorepo organization. Once you understand these patterns, you'll never wonder "where should this code go?" again. Let's start building this knowledge together!

Quick Reference

Core concepts:

apps/     → Deployable applications (dashboard, portal, api, mobile)
libs/ → Reusable code shared across applications

Library types:

  • shared/ - Code used by all applications
  • feature/ - Complete business features (auth, products, orders)
  • data-access/ - API clients and data fetching logic
  • ui/ - Presentational UI components
  • util/ - Pure utility functions and helpers

Naming conventions:

apps/dashboard                    # Simple app name
libs/shared/ui # scope/type
libs/feature/auth # type/name
libs/customer/feature-cart # scope/type-name

Golden rules:

  • ✅ Apps import from libraries
  • ❌ Apps NEVER import from other apps
  • ✅ Libraries import from other libraries (with care)
  • ❌ No circular dependencies between libraries

What You Need to Know First

To get the most out of this guide, you should understand:

Required:

  • Nx workspace basics: How to create and navigate an Nx workspace (see our Nx Workspace guide)
  • TypeScript imports: How import/export works in JavaScript/TypeScript
  • Basic software architecture: Understanding of separation of concerns

Helpful background:

Tools you'll need:

  • Existing Nx workspace (we'll use ShopHub example)
  • Code editor with TypeScript support
  • pnpm package manager

What We'll Cover in This Article

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

  • The fundamental difference between apps and libraries
  • When to create an app vs a library
  • The five main library types and their purposes
  • Naming conventions that scale
  • How to organize libraries by domain
  • Bounded contexts and domain-driven design
  • Project tags and dependency constraints
  • Real-world organizational patterns
  • Common mistakes and how to avoid them

What We'll Explain Along the Way

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

  • Deployable vs reusable code
  • Library scopes and domains
  • Import path mapping
  • Dependency graph visualization
  • Bounded contexts
  • Feature slicing

Understanding Apps vs Libraries

Let's start our journey by understanding the most fundamental concept in Nx organization: the difference between applications and libraries. This is the foundation everything else builds upon.

What Is an Application?

An application (or "app") is something you deploy—something that runs independently in production.

Think of it like this: An app is like a finished product on a store shelf. It's complete, packaged, ready to use. Customers (users) interact with apps directly.

Examples from ShopHub:

apps/
├── dashboard/ ← Admin dashboard (deployed to Vercel)
├── portal/ ← Customer website (deployed to Vercel)
├── api/ ← Backend API (deployed to AWS/VPS)
└── mobile/ ← Mobile app (deployed to App Store/Play Store)

Each application:

  • Has an entry point (main.ts, main.tsx, index.html)
  • Runs as a standalone process
  • Has its own URL or deployment target
  • Can be started and stopped independently
  • Users interact with it directly

Let's visualize an app's lifecycle:

Key insight: Apps are the outputs of your monorepo—the products you ship to users.

What Is a Library?

A library (or "lib") is reusable code that apps (and other libraries) import and use. Libraries are never deployed on their own.

Think of it like this: A library is like ingredients or components in a factory. You don't sell the ingredients—you use them to make finished products (apps).

Examples from ShopHub:

libs/
├── shared/
│ ├── ui/ ← Reusable React components
│ ├── types/ ← TypeScript type definitions
│ └── util/ ← Utility functions
├── feature/
│ ├── auth/ ← Authentication feature
│ ├── products/ ← Product catalog
│ └── orders/ ← Order management
└── data-access/
└── api-client/ ← HTTP client for backend

Each library:

  • Has an index.ts export file (public API)
  • Cannot run independently
  • Is imported by apps or other libraries
  • Contains focused, reusable code
  • Never deploys on its own

Let's visualize library usage:

Key insight: Libraries are the building blocks that apps assemble to create functionality.

The Crucial Difference: Deployable vs Reusable

Here's the simple question that determines whether something should be an app or library:

"Will this be deployed and accessed by users?"

  • YES → It's an app
  • NO → It's a library

Let's test your understanding with scenarios:

Scenario 1: Admin Dashboard

Question: App or Library?
Answer: App
Why? Users access it directly at dashboard.shophub.com
Deployed? Yes, to Vercel

Scenario 2: Button Component

Question: App or Library?
Answer: Library
Why? Users never access Button.tsx directly
Deployed? No, it's imported by apps

Scenario 3: Authentication System

Question: App or Library?
Answer: Library (probably!)
Why? Auth is used BY apps, not accessed directly
Deployed? No, it's code that apps import

Exception: If you have a separate auth service (like Auth0),
then that service would be an app

Scenario 4: Mobile Shopping App

Question: App or Library?
Answer: App
Why? Users download and run it on their phones
Deployed? Yes, to App Store and Google Play

Apps Cannot Import from Apps

This is one of the most important rules in Nx. Let me show you why with a real example.

❌ Wrong: App importing from another app

// apps/dashboard/src/app/utils.ts
export function formatPrice(price: number): string {
return `$${price.toFixed(2)}`;
}

// apps/portal/src/products/ProductCard.tsx
import { formatPrice } from "../../../apps/dashboard/src/app/utils"; // ❌ BAD!

export function ProductCard({ product }) {
return <div>{formatPrice(product.price)}</div>;
}

Why is this wrong?

  1. Tight coupling - Portal now depends on Dashboard
  2. Deployment nightmare - Can't deploy Portal without Dashboard code
  3. Circular dependencies - What if Dashboard wants to import from Portal?
  4. Confusing architecture - Apps should be independent

✅ Correct: Extract to library

// libs/shared/util/src/lib/format-price.ts
export function formatPrice(price: number): string {
return `$${price.toFixed(2)}`;
}

// libs/shared/util/src/index.ts
export * from "./lib/format-price";

// apps/dashboard/src/app/components/PriceDisplay.tsx
import { formatPrice } from "@shophub/shared/util";

export function PriceDisplay({ price }) {
return <span>{formatPrice(price)}</span>;
}

// apps/portal/src/products/ProductCard.tsx
import { formatPrice } from "@shophub/shared/util";

export function ProductCard({ product }) {
return <div>{formatPrice(product.price)}</div>;
}

Why is this correct?

  1. Loose coupling - Both apps depend on library, not each other
  2. Independent deployment - Apps deploy separately
  3. Clear architecture - Libraries provide shared functionality
  4. Reusability - Any app can use formatPrice

The rule: Shared code goes in libraries, never in apps!


The Five Library Types

Now let's discover the five main types of libraries you'll create in Nx. Each type serves a specific purpose and has clear guidelines for what belongs in it.

Think of these library types as different departments in a company—each has its specialty and knows exactly what it's responsible for.

1. Shared Libraries: The Foundation

Shared libraries contain code used across ALL or most applications in your workspace.

Think of it like this: Shared libraries are like your workspace's utility belt—tools that everyone needs and everyone can access.

What belongs in shared libraries:

libs/shared/
├── ui/ ← UI components (Button, Card, Modal)
├── types/ ← TypeScript interfaces (User, Product, Order)
├── util/ ← Pure functions (formatDate, validateEmail)
├── constants/ ← Shared constants (API_BASE_URL, MAX_FILE_SIZE)
└── config/ ← Configuration (feature flags, environments)

Real examples:

// libs/shared/types/src/lib/product.ts
/**
* Product entity used across dashboard, portal, and mobile
*/
export interface Product {
id: string;
name: string;
description: string;
price: number;
imageUrl: string;
category: string;
inStock: boolean;
createdAt: Date;
updatedAt: Date;
}

export interface ProductListResponse {
products: Product[];
total: number;
page: number;
pageSize: number;
}
// libs/shared/util/src/lib/formatting.ts
/**
* Format price with currency symbol
*/
export function formatPrice(price: number, currency: string = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency,
}).format(price);
}

/**
* Format date to human-readable string
*/
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
}

/**
* Truncate text to specified length
*/
export function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + "...";
}
// libs/shared/ui/src/lib/Button.tsx
import { ButtonHTMLAttributes, ReactNode } from "react";

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger";
size?: "small" | "medium" | "large";
children: ReactNode;
}

/**
* Reusable button component used across all apps
*/
export function Button({
variant = "primary",
size = "medium",
children,
className = "",
...props
}: ButtonProps) {
const baseClasses = "rounded font-semibold transition-colors";
const variantClasses = {
primary: "bg-blue-600 hover:bg-blue-700 text-white",
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
danger: "bg-red-600 hover:bg-red-700 text-white",
};
const sizeClasses = {
small: "px-3 py-1 text-sm",
medium: "px-4 py-2 text-base",
large: "px-6 py-3 text-lg",
};

return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
{...props}
>
{children}
</button>
);
}

When to create shared libraries:

✅ Code is used by 3+ applications ✅ Code has no business logic (pure utilities) ✅ Code is stable and changes infrequently ✅ Code is domain-agnostic

❌ Code is specific to one feature ❌ Code changes frequently ❌ Code has business logic ❌ Code is experimental

Creating a shared library:

# Generate shared UI library
nx generate @nx/react:library ui --directory=shared --tags=scope:shared,type:ui

# Generate shared types library
nx generate @nx/js:library types --directory=shared --tags=scope:shared,type:types

# Generate shared utilities library
nx generate @nx/js:library util --directory=shared --tags=scope:shared,type:util

2. Feature Libraries: Business Logic

Feature libraries contain complete features or business domains—everything needed for a specific area of functionality.

Think of it like this: A feature library is like a self-contained department in a company. It knows how to do one specific thing really well.

What belongs in feature libraries:

libs/feature/
├── auth/ ← Authentication & authorization
│ ├── login/
│ ├── register/
│ ├── password-reset/
│ └── guards/
├── products/ ← Product catalog management
│ ├── list/
│ ├── detail/
│ ├── search/
│ └── filters/
├── orders/ ← Order processing
│ ├── create/
│ ├── list/
│ ├── detail/
│ └── status-tracking/
└── cart/ ← Shopping cart
├── add-to-cart/
├── cart-view/
└── checkout/

Real example: Authentication feature

// libs/feature/auth/src/lib/login/LoginForm.tsx
import { useState } from "react";
import { Button } from "@shophub/shared/ui";
import { useAuthService } from "../services/auth.service";

export function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { login, loading, error } = useAuthService();

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login(email, password);
};

return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full rounded border-gray-300"
required
/>
</div>

<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full rounded border-gray-300"
required
/>
</div>

{error && <div className="text-red-600 text-sm">{error}</div>}

<Button type="submit" disabled={loading} className="w-full">
{loading ? "Logging in..." : "Log In"}
</Button>
</form>
);
}
// libs/feature/auth/src/lib/services/auth.service.ts
import { useState } from "react";
import { User, AuthResponse } from "@shophub/shared/types";
import { apiClient } from "@shophub/data-access/api-client";

export function useAuthService() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const login = async (email: string, password: string): Promise<void> => {
setLoading(true);
setError(null);

try {
const response = await apiClient.post<AuthResponse>("/auth/login", {
email,
password,
});

setUser(response.user);
localStorage.setItem("token", response.token);
localStorage.setItem("user", JSON.stringify(response.user));
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed");
throw err;
} finally {
setLoading(false);
}
};

const logout = (): void => {
setUser(null);
localStorage.removeItem("token");
localStorage.removeItem("user");
};

const register = async (
email: string,
password: string,
name: string
): Promise<void> => {
setLoading(true);
setError(null);

try {
const response = await apiClient.post<AuthResponse>("/auth/register", {
email,
password,
name,
});

setUser(response.user);
localStorage.setItem("token", response.token);
localStorage.setItem("user", JSON.stringify(response.user));
} catch (err) {
setError(err instanceof Error ? err.message : "Registration failed");
throw err;
} finally {
setLoading(false);
}
};

return {
user,
loading,
error,
login,
logout,
register,
};
}
// libs/feature/auth/src/lib/guards/AuthGuard.tsx
import { ReactNode } from "react";
import { Navigate } from "react-router-dom";
import { useAuthService } from "../services/auth.service";

interface AuthGuardProps {
children: ReactNode;
redirectTo?: string;
}

/**
* Protects routes that require authentication
*/
export function AuthGuard({ children, redirectTo = "/login" }: AuthGuardProps) {
const { user } = useAuthService();

if (!user) {
return <Navigate to={redirectTo} replace />;
}

return <>{children}</>;
}

When to create feature libraries:

✅ Complete business feature (auth, products, orders) ✅ Contains UI components + logic + data fetching ✅ Used by multiple apps ✅ Represents a bounded context

❌ Just a single component ❌ Pure utility functions ❌ Just type definitions

Creating a feature library:

# Generate authentication feature
nx generate @nx/react:library auth --directory=feature --tags=type:feature

# Generate products feature
nx generate @nx/react:library products --directory=feature --tags=type:feature

# Generate orders feature
nx generate @nx/react:library orders --directory=feature --tags=type:feature

3. Data Access Libraries: API Communication

Data access libraries handle all communication with external services—APIs, databases, external services.

Think of it like this: Data access libraries are your workspace's courier service—they know how to fetch and send data from/to the outside world.

What belongs in data-access libraries:

libs/data-access/
├── api-client/ ← HTTP client configuration
│ ├── client.ts
│ ├── interceptors.ts
│ └── error-handling.ts
├── products-api/ ← Product API endpoints
│ ├── get-products.ts
│ ├── get-product-by-id.ts
│ └── create-product.ts
├── orders-api/ ← Order API endpoints
│ ├── create-order.ts
│ ├── get-orders.ts
│ └── update-order-status.ts
└── websocket/ ← Real-time communication
├── socket-client.ts
└── event-handlers.ts

Real example: API client

// libs/data-access/api-client/src/lib/client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";

/**
* Base API client for all HTTP requests
*/
class ApiClient {
private client: AxiosInstance;

constructor(baseURL: string) {
this.client = axios.create({
baseURL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});

this.setupInterceptors();
}

private setupInterceptors(): void {
// Request interceptor - add auth token
this.client.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);

// Response interceptor - handle errors
this.client.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Token expired, redirect to login
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(error);
}
);
}

async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<T>(url, config);
return response.data;
}

async post<T>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<T> {
const response = await this.client.post<T>(url, data, config);
return response.data;
}

async put<T>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<T> {
const response = await this.client.put<T>(url, data, config);
return response.data;
}

async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.delete<T>(url, config);
return response.data;
}
}

// Export singleton instance
export const apiClient = new ApiClient(
process.env.NX_API_URL || "http://localhost:3333/api"
);
// libs/data-access/products-api/src/lib/products.api.ts
import { apiClient } from "@shophub/data-access/api-client";
import {
Product,
ProductListResponse,
CreateProductRequest,
} from "@shophub/shared/types";

/**
* Fetch paginated list of products
*/
export async function getProducts(
page: number = 1,
pageSize: number = 20,
category?: string
): Promise<ProductListResponse> {
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString(),
...(category && { category }),
});

return apiClient.get<ProductListResponse>(`/products?${params}`);
}

/**
* Fetch single product by ID
*/
export async function getProductById(id: string): Promise<Product> {
return apiClient.get<Product>(`/products/${id}`);
}

/**
* Create new product
*/
export async function createProduct(
data: CreateProductRequest
): Promise<Product> {
return apiClient.post<Product>("/products", data);
}

/**
* Update existing product
*/
export async function updateProduct(
id: string,
data: Partial<Product>
): Promise<Product> {
return apiClient.put<Product>(`/products/${id}`, data);
}

/**
* Delete product
*/
export async function deleteProduct(id: string): Promise<void> {
return apiClient.delete(`/products/${id}`);
}

/**
* Search products by query
*/
export async function searchProducts(query: string): Promise<Product[]> {
return apiClient.get<Product[]>(
`/products/search?q=${encodeURIComponent(query)}`
);
}

When to create data-access libraries:

✅ HTTP API calls ✅ WebSocket connections ✅ Database queries ✅ External service integrations

❌ Business logic (goes in features) ❌ UI components (goes in ui libraries) ❌ Utilities (goes in util libraries)

Creating data-access libraries:

# Generate API client library
nx generate @nx/js:library api-client --directory=data-access --tags=type:data-access

# Generate products API library
nx generate @nx/js:library products-api --directory=data-access --tags=type:data-access

# Generate orders API library
nx generate @nx/js:library orders-api --directory=data-access --tags=type:data-access

4. UI Libraries: Presentational Components

UI libraries contain purely presentational components—components that receive data via props and render it, with no business logic.

Think of it like this: UI libraries are like actors in a play—they perform what they're told, but don't write the script.

What belongs in UI libraries:

libs/shared/ui/
├── button/
│ ├── Button.tsx
│ ├── Button.spec.tsx
│ └── Button.stories.tsx
├── card/
│ ├── Card.tsx
│ └── Card.spec.tsx
├── modal/
│ ├── Modal.tsx
│ └── Modal.spec.tsx
├── form/
│ ├── Input.tsx
│ ├── Select.tsx
│ ├── Checkbox.tsx
│ └── TextArea.tsx
└── layout/
├── Header.tsx
├── Footer.tsx
└── Sidebar.tsx

Real example: Card component

// libs/shared/ui/src/lib/card/Card.tsx
import { ReactNode } from "react";

interface CardProps {
title?: string;
children: ReactNode;
footer?: ReactNode;
className?: string;
onClick?: () => void;
}

/**
* Reusable card component for displaying content
*/
export function Card({
title,
children,
footer,
className = "",
onClick,
}: CardProps) {
const handleClick = onClick ? { onClick, role: "button", tabIndex: 0 } : {};

return (
<div
className={`bg-white rounded-lg shadow-md overflow-hidden ${
onClick ? "cursor-pointer hover:shadow-lg transition-shadow" : ""
} ${className}`}
{...handleClick}
>
{title && (
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>
)}

<div className="px-6 py-4">{children}</div>

{footer && (
<div className="px-6 py-4 bg-gray-50 border-t border-gray-200">
{footer}
</div>
)}
</div>
);
}
// libs/shared/ui/src/lib/modal/Modal.tsx
import { ReactNode, useEffect } from "react";
import { Button } from "../button/Button";

interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
footer?: ReactNode;
size?: "small" | "medium" | "large";
}

/**
* Modal dialog component
*/
export function Modal({
isOpen,
onClose,
title,
children,
footer,
size = "medium",
}: ModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}

return () => {
document.body.style.overflow = "unset";
};
}, [isOpen]);

if (!isOpen) return null;

const sizeClasses = {
small: "max-w-md",
medium: "max-w-2xl",
large: "max-w-4xl",
};

return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black bg-opacity-50"
onClick={onClose}
/>

{/* Modal */}
<div
className={`relative bg-white rounded-lg shadow-xl ${sizeClasses[size]} w-full mx-4`}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">{title}</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Close modal"
>
<svg
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>

{/* Body */}
<div className="px-6 py-4 max-h-[70vh] overflow-y-auto">{children}</div>

{/* Footer */}
{footer && (
<div className="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-end gap-3">
{footer}
</div>
)}
</div>
</div>
);
}

Characteristics of UI components:

Presentational only - No business logic ✅ Receive data via props - Don't fetch their own data ✅ Reusable - Work in any context ✅ Well-tested - Easy to test in isolation ✅ Documented - Clear prop types and examples

❌ No API calls ❌ No state management (except UI state) ❌ No routing logic ❌ No business rules

5. Util Libraries: Pure Functions

Util libraries contain pure utility functions—functions that take input, return output, and have no side effects.

Think of it like this: Util libraries are like a toolbox—each tool does one specific job really well.

What belongs in util libraries:

libs/shared/util/
├── formatting/
│ ├── format-date.ts
│ ├── format-price.ts
│ └── format-phone.ts
├── validation/
│ ├── validate-email.ts
│ ├── validate-phone.ts
│ └── validate-password.ts
├── string/
│ ├── truncate.ts
│ ├── capitalize.ts
│ └── slugify.ts
└── array/
├── group-by.ts
├── sort-by.ts
└── unique.ts

Real examples:

// libs/shared/util/src/lib/validation/validate-email.ts
/**
* Validate email address format
*/
export function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}

/**
* Get validation error message for email
*/
export function getEmailValidationError(email: string): string | null {
if (!email) {
return "Email is required";
}
if (!validateEmail(email)) {
return "Please enter a valid email address";
}
return null;
}
// libs/shared/util/src/lib/validation/validate-password.ts
interface PasswordStrength {
score: number; // 0-4
feedback: string[];
isStrong: boolean;
}

/**
* Calculate password strength
*/
export function calculatePasswordStrength(password: string): PasswordStrength {
let score = 0;
const feedback: string[] = [];

if (!password) {
return { score: 0, feedback: ["Password is required"], isStrong: false };
}

// Length check
if (password.length >= 8) score++;
else feedback.push("Use at least 8 characters");

if (password.length >= 12) score++;

// Complexity checks
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) {
score++;
} else {
feedback.push("Use both uppercase and lowercase letters");
}

if (/\d/.test(password)) {
score++;
} else {
feedback.push("Include at least one number");
}

if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
score++;
} else {
feedback.push("Include at least one special character");
}

return {
score: Math.min(score, 4),
feedback,
isStrong: score >= 3,
};
}
// libs/shared/util/src/lib/string/slugify.ts
/**
* Convert string to URL-friendly slug
* Example: "Hello World!" -> "hello-world"
*/
export function slugify(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "") // Remove special characters
.replace(/\s+/g, "-") // Replace spaces with hyphens
.replace(/-+/g, "-"); // Remove consecutive hyphens
}

/**
* Reverse slug to readable text
* Example: "hello-world" -> "Hello World"
*/
export function unslugify(slug: string): string {
return slug
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
// libs/shared/util/src/lib/array/group-by.ts
/**
* Group array items by a key
*/
export function groupBy<T>(
array: T[],
key: keyof T | ((item: T) => string | number)
): Record<string, T[]> {
return array.reduce((result, item) => {
const groupKey =
typeof key === "function" ? String(key(item)) : String(item[key]);

if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
return result;
}, {} as Record<string, T[]>);
}

// Example usage:
// const products = [
// { id: 1, name: 'Laptop', category: 'Electronics' },
// { id: 2, name: 'Phone', category: 'Electronics' },
// { id: 3, name: 'Shirt', category: 'Clothing' }
// ];
//
// groupBy(products, 'category')
// Result: {
// Electronics: [{ id: 1, ... }, { id: 2, ... }],
// Clothing: [{ id: 3, ... }]
// }

Characteristics of util functions:

Pure functions - Same input = same output ✅ No side effects - Don't modify external state ✅ Well-tested - Easy to unit test ✅ Single responsibility - Do one thing well ✅ Generic - Work with any data

❌ No API calls ❌ No DOM manipulation ❌ No state management ❌ No side effects


Organizing Libraries by Domain

Now that we understand library types, let's discover how to organize them by domain. This is where monorepo organization becomes an art form.

What Is a Domain?

A domain represents a business area or bounded context—a cohesive set of features that belong together.

Think of it like this: In a shopping mall, domains are like stores—each store focuses on a specific category (electronics, clothing, food). You wouldn't find laptops in the food court!

ShopHub domains:

Customer Domain:
- Product browsing
- Shopping cart
- Checkout
- Order tracking

Admin Domain:
- Product management
- Order fulfillment
- User management
- Analytics

Shared Domain:
- Authentication
- Notifications
- Search
- Payments

Domain-Based Library Organization

Let's organize our libraries by domain:

libs/
├── customer/ ← Customer-facing features
│ ├── feature-products/ ← Product catalog for customers
│ ├── feature-cart/ ← Shopping cart
│ ├── feature-checkout/ ← Checkout flow
│ └── feature-orders/ ← Order history
├── admin/ ← Admin-specific features
│ ├── feature-products/ ← Product management
│ ├── feature-orders/ ← Order fulfillment
│ ├── feature-users/ ← User management
│ └── feature-analytics/ ← Business analytics
├── shared/ ← Used by all domains
│ ├── ui/ ← Common UI components
│ ├── types/ ← Shared type definitions
│ ├── util/ ← Utility functions
│ └── config/ ← Configuration
├── feature/ ← Cross-domain features
│ ├── auth/ ← Authentication (all apps)
│ ├── notifications/ ← Notifications (all apps)
│ └── search/ ← Search (customer + admin)
└── data-access/ ← API communication
├── api-client/ ← HTTP client
├── products-api/ ← Product endpoints
└── orders-api/ ← Order endpoints

Now let's see which apps use which domains:

Notice the pattern:

  • Dashboard (admin) → Uses admin domain
  • Portal (customer) → Uses customer domain
  • Mobile (customer) → Uses customer domain
  • API → Uses shared types only
  • Everyone → Uses shared utilities and auth

Real-World Example: Feature Library Structure

Let's create a complete feature library with proper domain organization:

# Generate customer product catalog feature
nx generate @nx/react:library feature-products \
--directory=customer \
--tags=scope:customer,type:feature

# Generate admin product management feature
nx generate @nx/react:library feature-products \
--directory=admin \
--tags=scope:admin,type:feature

Customer product feature structure:

libs/customer/feature-products/
├── src/
│ ├── index.ts ← Public exports
│ ├── lib/
│ │ ├── product-list/ ← Product listing component
│ │ │ ├── ProductList.tsx
│ │ │ ├── ProductList.spec.tsx
│ │ │ └── useProductList.ts ← Custom hook
│ │ ├── product-detail/ ← Product detail view
│ │ │ ├── ProductDetail.tsx
│ │ │ ├── ProductDetail.spec.tsx
│ │ │ └── useProductDetail.ts
│ │ ├── product-filters/ ← Filtering UI
│ │ │ ├── ProductFilters.tsx
│ │ │ └── ProductFilters.spec.tsx
│ │ └── product-search/ ← Search functionality
│ │ ├── ProductSearch.tsx
│ │ └── ProductSearch.spec.tsx
├── project.json
├── tsconfig.json
└── README.md

Admin product feature structure:

libs/admin/feature-products/
├── src/
│ ├── index.ts
│ ├── lib/
│ │ ├── product-management/ ← Product CRUD operations
│ │ │ ├── ProductManagement.tsx
│ │ │ └── ProductManagement.spec.tsx
│ │ ├── product-form/ ← Create/Edit form
│ │ │ ├── ProductForm.tsx
│ │ │ ├── ProductForm.spec.tsx
│ │ │ └── useProductForm.ts
│ │ ├── product-table/ ← Admin product table
│ │ │ ├── ProductTable.tsx
│ │ │ └── ProductTable.spec.tsx
│ │ └── product-analytics/ ← Product performance metrics
│ │ ├── ProductAnalytics.tsx
│ │ └── ProductAnalytics.spec.tsx
├── project.json
└── tsconfig.json

Notice the difference:

  • Customer features: Browse, view, search (read-only)
  • Admin features: CRUD operations, management, analytics

Exporting from Libraries

Each library has an index.ts file that defines its public API:

// libs/customer/feature-products/src/index.ts
/**
* Customer Product Catalog Feature
*
* This library provides components for browsing and viewing products
* in the customer-facing portal and mobile apps.
*/

// Export main components
export { ProductList } from "./lib/product-list/ProductList";
export { ProductDetail } from "./lib/product-detail/ProductDetail";
export { ProductFilters } from "./lib/product-filters/ProductFilters";
export { ProductSearch } from "./lib/product-search/ProductSearch";

// Export custom hooks
export { useProductList } from "./lib/product-list/useProductList";
export { useProductDetail } from "./lib/product-detail/useProductDetail";

// Export types specific to this feature
export type { ProductListProps } from "./lib/product-list/ProductList";
export type { ProductDetailProps } from "./lib/product-detail/ProductDetail";
export type { ProductFiltersProps } from "./lib/product-filters/ProductFilters";

Why is this important?

  1. Clear public API - Only what's exported is accessible
  2. Encapsulation - Internal implementation details stay private
  3. Easier refactoring - Change internals without breaking consumers
  4. Better tree-shaking - Unused exports can be removed from bundles

Using the library:

// apps/portal/src/pages/products/ProductsPage.tsx
import {
ProductList,
ProductFilters,
} from "@shophub/customer/feature-products";
import { useState } from "react";

export function ProductsPage() {
const [filters, setFilters] = useState({});

return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">Our Products</h1>

<div className="grid grid-cols-4 gap-6">
<aside className="col-span-1">
<ProductFilters filters={filters} onChange={setFilters} />
</aside>

<main className="col-span-3">
<ProductList filters={filters} />
</main>
</div>
</div>
);
}

Naming Conventions That Scale

Let's discover naming conventions that remain clear even when you have 100+ libraries.

Library Naming Pattern

The standard pattern for library names:

[scope]/[type]-[name]

Examples:

Good naming:
✅ libs/shared/ui → shared/ui
✅ libs/shared/types → shared/types
✅ libs/customer/feature-products → customer/feature-products
✅ libs/admin/feature-orders → admin/feature-orders
✅ libs/data-access/api-client → data-access/api-client

Bad naming:
❌ libs/ui → Too generic
❌ libs/products → Missing scope and type
❌ libs/customer-products → Unclear structure
❌ libs/the-product-feature → Unnecessary words

Import Path Mapping

Nx automatically creates TypeScript path mappings for your libraries:

// tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@shophub/shared/ui": ["libs/shared/ui/src/index.ts"],
"@shophub/shared/types": ["libs/shared/types/src/index.ts"],
"@shophub/shared/util": ["libs/shared/util/src/index.ts"],
"@shophub/customer/feature-products": [
"libs/customer/feature-products/src/index.ts"
],
"@shophub/admin/feature-products": [
"libs/admin/feature-products/src/index.ts"
],
"@shophub/feature/auth": ["libs/feature/auth/src/index.ts"],
"@shophub/data-access/api-client": [
"libs/data-access/api-client/src/index.ts"
]
}
}
}

Notice the pattern: @workspace-name/library-path

Directory Structure Best Practices

Let's look at a complete, well-organized workspace:

shophub-platform/
├── apps/
│ ├── dashboard/ ← Admin dashboard (React + Vite)
│ ├── portal/ ← Customer website (Next.js)
│ ├── api/ ← Backend API (NestJS)
│ └── mobile/ ← Mobile app (React Native)
├── libs/
│ ├── customer/ ← Customer domain
│ │ ├── feature-products/
│ │ ├── feature-cart/
│ │ ├── feature-checkout/
│ │ └── feature-orders/
│ ├── admin/ ← Admin domain
│ │ ├── feature-products/
│ │ ├── feature-orders/
│ │ ├── feature-users/
│ │ └── feature-analytics/
│ ├── shared/ ← Shared across all
│ │ ├── ui/ ← UI components
│ │ │ ├── button/
│ │ │ ├── card/
│ │ │ ├── modal/
│ │ │ └── form/
│ │ ├── types/ ← Type definitions
│ │ │ ├── user.ts
│ │ │ ├── product.ts
│ │ │ └── order.ts
│ │ ├── util/ ← Utilities
│ │ │ ├── formatting/
│ │ │ ├── validation/
│ │ │ └── string/
│ │ └── config/ ← Configuration
│ ├── feature/ ← Cross-domain features
│ │ ├── auth/
│ │ ├── notifications/
│ │ └── search/
│ └── data-access/ ← API communication
│ ├── api-client/
│ ├── products-api/
│ └── orders-api/
├── tools/ ← Custom scripts
│ ├── generators/
│ └── scripts/
├── .github/
│ └── workflows/ ← CI/CD pipelines
├── nx.json
├── package.json
├── pnpm-lock.yaml
└── tsconfig.base.json

Project Tags and Dependency Constraints

Now let's discover how to enforce architectural rules using project tags. This is where Nx becomes a guardian of your code quality.

Understanding Project Tags

Project tags are labels you add to projects that help you:

  1. Categorize projects by scope (customer, admin, shared)
  2. Categorize projects by type (app, feature, ui, util)
  3. Enforce dependency rules
  4. Run commands on specific groups

Think of it like this: Tags are like labels in a filing cabinet—they help you organize and find things, plus they let you set rules like "confidential files can only go in locked drawers."

Adding Tags to Projects

Tags are defined in each project's project.json:

// apps/dashboard/project.json
{
"name": "dashboard",
"tags": ["scope:admin", "type:app", "platform:web"]
}

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

// apps/api/project.json
{
"name": "api",
"tags": ["scope:shared", "type:app", "platform:server"]
}

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

// libs/admin/feature-products/project.json
{
"name": "admin-feature-products",
"tags": ["scope:admin", "type:feature"]
}

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

// libs/data-access/api-client/project.json
{
"name": "data-access-api-client",
"tags": ["scope:shared", "type:data-access"]
}

Enforcing Dependency Rules

Now let's set up rules in ESLint to enforce proper dependencies:

// .eslintrc.json
{
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
// Rule 1: Customer scope can only use customer and shared libraries
{
"sourceTag": "scope:customer",
"onlyDependOnLibsWithTags": ["scope:customer", "scope:shared"]
},
// Rule 2: Admin scope can only use admin and shared libraries
{
"sourceTag": "scope:admin",
"onlyDependOnLibsWithTags": ["scope:admin", "scope:shared"]
},
// Rule 3: Shared libraries can only depend on other shared libraries
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
},
// Rule 4: Apps can only depend on features, not other apps
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": [
"type:feature",
"type:ui",
"type:util",
"type:data-access"
]
},
// Rule 5: Feature libraries can depend on ui, util, data-access
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": [
"type:feature",
"type:ui",
"type:util",
"type:data-access"
]
},
// Rule 6: UI libraries can only depend on util libraries
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui", "type:util"]
},
// Rule 7: Util libraries cannot depend on anything
{
"sourceTag": "type:util",
"onlyDependOnLibsWithTags": ["type:util"]
},
// Rule 8: Data-access can depend on util libraries
{
"sourceTag": "type:data-access",
"onlyDependOnLibsWithTags": ["type:data-access", "type:util"]
}
]
}
]
}
}
]
}

What These Rules Enforce

Let's understand what each rule prevents:

Rule 1: Customer isolation

// libs/customer/feature-products/src/lib/ProductList.tsx
import { AdminTable } from "@shophub/admin/feature-products"; // ❌ LINT ERROR!
// "Projects tagged with scope:customer can only depend on libs tagged with scope:customer or scope:shared"

// ✅ Correct:
import { Card } from "@shophub/shared/ui";
import { formatPrice } from "@shophub/shared/util";

Rule 2: Admin isolation

// libs/admin/feature-orders/src/lib/OrderManagement.tsx
import { CartButton } from "@shophub/customer/feature-cart"; // ❌ LINT ERROR!
// "Projects tagged with scope:admin can only depend on libs tagged with scope:admin or scope:shared"

// ✅ Correct:
import { Button } from "@shophub/shared/ui";

Rule 4: Apps cannot import from apps

// apps/portal/src/pages/Home.tsx
import { DashboardWidget } from "../../../dashboard/src/app/widgets"; // ❌ LINT ERROR!
// "Projects tagged with type:app can only depend on libs tagged with type:feature, type:ui, type:util, or type:data-access"

// ✅ Correct:
import { ProductList } from "@shophub/customer/feature-products";

Rule 6: UI libraries stay pure

// libs/shared/ui/src/lib/button/Button.tsx
import { getProducts } from "@shophub/data-access/products-api"; // ❌ LINT ERROR!
// "Projects tagged with type:ui can only depend on libs tagged with type:ui or type:util"

// ✅ Correct:
import { formatDate } from "@shophub/shared/util";

Visualizing Dependency Rules

Let's see these rules visually:

The hierarchy:

  1. Apps (top) - Can import from features
  2. Features (middle) - Can import from ui, data-access, util
  3. UI, Data-Access (lower) - Can import from util
  4. Util (bottom) - Pure, no dependencies

Common Mistakes and How to Avoid Them

Let's learn from common mistakes developers make when organizing monorepos. I'll show you what goes wrong and how to fix it.

Mistake 1: Creating Too Many Tiny Libraries

The problem:

libs/
├── format-date/ ← Just one function!
├── format-price/ ← Just one function!
├── format-phone/ ← Just one function!
├── validate-email/ ← Just one function!
├── validate-phone/ ← Just one function!
├── validate-password/ ← Just one function!
... (50+ tiny libraries)

Why it's bad:

  • Overwhelming number of libraries
  • Hard to find what you need
  • Import statements become messy
  • Slower builds (more projects to process)

The fix:

libs/shared/util/
├── formatting/
│ ├── format-date.ts
│ ├── format-price.ts
│ └── format-phone.ts
└── validation/
├── validate-email.ts
├── validate-phone.ts
└── validate-password.ts

Rule of thumb: A library should have at least 500-1000 lines of related code, or provide a cohesive feature.

Mistake 2: Not Planning for Growth

The problem:

# Starting simple
my-workspace/
├── app1/
├── app2/
├── lib1/
├── lib2/
... (flat structure)

# Six months later
my-workspace/
├── app1/
├── app2/
├── app3/
├── app4/
├── lib1/
├── lib2/
... (30 projects at root - chaos!)

Why it's bad:

  • Becomes unmanageable
  • No clear ownership
  • Difficult to navigate
  • Hard to refactor later

The fix - start with organization:

my-workspace/
├── apps/
│ ├── customer/
│ │ ├── web/
│ │ └── mobile/
│ └── admin/
│ └── dashboard/
├── libs/
│ ├── customer/
│ │ ├── feature-products/
│ │ └── feature-cart/
│ ├── admin/
│ │ └── feature-users/
│ └── shared/
│ ├── ui/
│ └── util/
└── tools/

Rule: Group by domain from day one. It's easier to start organized than reorganize later.

Mistake 3: Mixing Concerns in Libraries

The problem:

// libs/shared/everything/src/index.ts
export { Button } from "./ui/Button"; // UI component
export { formatDate } from "./utils/date"; // Utility function
export { Product } from "./types/product"; // Type definition
export { getProducts } from "./api/products"; // API call
export { useAuth } from "./hooks/auth"; // Business logic

// This library does EVERYTHING!

Why it's bad:

  • No clear responsibility
  • Hard to understand what it does
  • Difficult to test
  • Poor tree-shaking (imports everything)
  • Can't enforce dependency rules

The fix - separate by type:

// libs/shared/ui/src/index.ts
export { Button } from "./lib/Button";

// libs/shared/util/src/index.ts
export { formatDate } from "./lib/formatting/date";

// libs/shared/types/src/index.ts
export { Product } from "./lib/product";

// libs/data-access/products-api/src/index.ts
export { getProducts } from "./lib/get-products";

// libs/feature/auth/src/index.ts
export { useAuth } from "./lib/hooks/use-auth";

Rule: Each library has ONE clear responsibility. If you can't describe it in one sentence, it's too complex.

Mistake 4: Not Using Path Mappings

The problem:

// apps/portal/src/pages/products/ProductList.tsx
import { Product } from "../../../../../libs/shared/types/src/lib/product";
import { formatPrice } from "../../../../../libs/shared/util/src/lib/formatting/price";
import { Button } from "../../../../../libs/shared/ui/src/lib/button/Button";

// Nightmare to maintain!

Why it's bad:

  • Hard to read
  • Breaks when files move
  • Error-prone (counting ../)
  • Poor developer experience

The fix - use path mappings:

// apps/portal/src/pages/products/ProductList.tsx
import { Product } from "@shophub/shared/types";
import { formatPrice } from "@shophub/shared/util";
import { Button } from "@shophub/shared/ui";

// Clean, maintainable, refactor-safe!

Nx sets these up automatically in tsconfig.base.json.

Mistake 5: Circular Dependencies

The problem:

// libs/feature/products/src/index.ts
import { addToCart } from "@shophub/feature/cart";

export function buyProduct(product: Product) {
// Uses cart functionality
addToCart(product);
}

// libs/feature/cart/src/index.ts
import { getProductDetails } from "@shophub/feature/products";

export function addToCart(product: Product) {
// Needs to fetch full product details
const details = getProductDetails(product.id);
// Now we have a circular dependency!
}

// Result: Build fails or runtime errors

Why it's bad:

  • Build failures
  • Runtime errors
  • Confusing dependency graph
  • Can't deploy independently
  • Impossible to understand flow

The fix - extract shared code:

// libs/shared/types/src/lib/product.ts
export interface Product {
id: string;
name: string;
price: number;
}

// libs/feature/products/src/index.ts
import { Product } from "@shophub/shared/types";

export function getProductDetails(id: string): Product {
// Fetch product
}

// libs/feature/cart/src/index.ts
import { Product } from "@shophub/shared/types";

export function addToCart(product: Product) {
// Use product type, don't import from products feature
}

Rule: If two features depend on each other, extract the shared parts into a library both can depend on.

Detecting circular dependencies:

# Visualize dependencies
nx graph

# Look for red lines - those indicate circular dependencies!

Mistake 6: Poor Library Boundaries

The problem:

// libs/shared/ui/src/lib/ProductCard.tsx
import { addToCart } from "@shophub/feature/cart"; // ❌ UI importing business logic!
import { getProducts } from "@shophub/data-access/products-api"; // ❌ UI fetching data!

export function ProductCard({ productId }: { productId: string }) {
const [product, setProduct] = useState(null);

useEffect(() => {
getProducts(productId).then(setProduct); // UI shouldn't fetch
}, [productId]);

return (
<div>
<h3>{product?.name}</h3>
<button onClick={() => addToCart(product)}>
{" "}
// UI shouldn't have logic Add to Cart
</button>
</div>
);
}

Why it's bad:

  • UI components become tightly coupled
  • Can't reuse in different contexts
  • Hard to test
  • Violates separation of concerns

The fix - pure presentational component:

// libs/shared/ui/src/lib/ProductCard.tsx
import { Product } from "@shophub/shared/types";

interface ProductCardProps {
product: Product;
onAddToCart: (product: Product) => void;
}

export function ProductCard({ product, onAddToCart }: ProductCardProps) {
return (
<div className="product-card">
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
<button onClick={() => onAddToCart(product)}>Add to Cart</button>
</div>
);
}

// Feature library connects everything:
// libs/customer/feature-products/src/lib/ProductListContainer.tsx
import { ProductCard } from "@shophub/shared/ui";
import { useProducts } from "./hooks/use-products";
import { useCart } from "@shophub/feature/cart";

export function ProductListContainer() {
const { products, loading } = useProducts();
const { addToCart } = useCart();

if (loading) return <div>Loading...</div>;

return (
<div className="product-grid">
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={addToCart}
/>
))}
</div>
);
}

Rule: UI libraries receive data via props and notify via callbacks. No business logic, no data fetching.


Practical Example: Building a Complete Feature

Let's put everything together by building a complete feature from scratch. We'll create the "Product Catalog" feature with proper structure.

Step 1: Plan the Feature

What we're building:

  • Product listing with filters
  • Product detail view
  • Search functionality
  • Add to cart integration

Libraries we need:

  1. Shared types (Product interface)
  2. Shared UI components (Card, Button)
  3. Data access (Product API)
  4. Customer feature (Product catalog)

Step 2: Create Shared Types

# Generate types library if not exists
nx generate @nx/js:library types --directory=shared --tags=scope:shared,type:types
// libs/shared/types/src/lib/product.ts
export interface Product {
id: string;
name: string;
description: string;
price: number;
imageUrl: string;
category: string;
inStock: boolean;
rating: number;
reviewCount: number;
createdAt: Date;
updatedAt: Date;
}

export interface ProductFilters {
category?: string;
minPrice?: number;
maxPrice?: number;
inStockOnly?: boolean;
searchQuery?: string;
}

export interface ProductListResponse {
products: Product[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}

// Export from index
// libs/shared/types/src/index.ts
export * from "./lib/product";

Step 3: Create Data Access Layer

# Generate products API library
nx generate @nx/js:library products-api --directory=data-access --tags=scope:shared,type:data-access
// libs/data-access/products-api/src/lib/products.api.ts
import { apiClient } from "@shophub/data-access/api-client";
import {
Product,
ProductListResponse,
ProductFilters,
} from "@shophub/shared/types";

/**
* Fetch paginated list of products with filters
*/
export async function getProducts(
page: number = 1,
pageSize: number = 20,
filters?: ProductFilters
): Promise<ProductListResponse> {
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString(),
});

if (filters?.category) {
params.append("category", filters.category);
}
if (filters?.minPrice !== undefined) {
params.append("minPrice", filters.minPrice.toString());
}
if (filters?.maxPrice !== undefined) {
params.append("maxPrice", filters.maxPrice.toString());
}
if (filters?.inStockOnly) {
params.append("inStockOnly", "true");
}
if (filters?.searchQuery) {
params.append("q", filters.searchQuery);
}

return apiClient.get<ProductListResponse>(`/products?${params}`);
}

/**
* Fetch single product by ID
*/
export async function getProductById(id: string): Promise<Product> {
return apiClient.get<Product>(`/products/${id}`);
}

/**
* Get related products
*/
export async function getRelatedProducts(
productId: string
): Promise<Product[]> {
return apiClient.get<Product[]>(`/products/${productId}/related`);
}

// Export from index
// libs/data-access/products-api/src/index.ts
export * from "./lib/products.api";

Step 4: Create Customer Feature Library

# Generate customer products feature
nx generate @nx/react:library feature-products --directory=customer --tags=scope:customer,type:feature
// libs/customer/feature-products/src/lib/hooks/use-products.ts
import { useState, useEffect } from "react";
import {
Product,
ProductListResponse,
ProductFilters,
} from "@shophub/shared/types";
import { getProducts } from "@shophub/data-access/products-api";

export function useProducts(filters?: ProductFilters) {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [total, setTotal] = useState(0);

useEffect(() => {
const fetchProducts = async () => {
setLoading(true);
setError(null);

try {
const response = await getProducts(page, 20, filters);
setProducts(response.products);
setTotal(response.total);
setHasMore(response.hasMore);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to load products"
);
} finally {
setLoading(false);
}
};

fetchProducts();
}, [page, filters]);

const loadMore = () => {
if (!loading && hasMore) {
setPage((prev) => prev + 1);
}
};

const reset = () => {
setPage(1);
setProducts([]);
};

return {
products,
loading,
error,
total,
hasMore,
loadMore,
reset,
};
}
// libs/customer/feature-products/src/lib/components/ProductList.tsx
import { Product } from "@shophub/shared/types";
import { Card, Button } from "@shophub/shared/ui";
import { formatPrice } from "@shophub/shared/util";

interface ProductListProps {
products: Product[];
onProductClick: (product: Product) => void;
onAddToCart: (product: Product) => void;
}

export function ProductList({
products,
onProductClick,
onAddToCart,
}: ProductListProps) {
if (products.length === 0) {
return (
<div className="text-center py-12">
<p className="text-gray-500 text-lg">No products found</p>
</div>
);
}

return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map((product) => (
<Card
key={product.id}
className="cursor-pointer"
onClick={() => onProductClick(product)}
>
<div className="aspect-square relative mb-4">
<img
src={product.imageUrl}
alt={product.name}
className="w-full h-full object-cover rounded"
/>
{!product.inStock && (
<div className="absolute top-2 right-2 bg-red-600 text-white px-3 py-1 rounded text-sm">
Out of Stock
</div>
)}
</div>

<h3 className="font-semibold text-lg mb-2">{product.name}</h3>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
{product.description}
</p>

<div className="flex items-center justify-between mb-3">
<span className="text-2xl font-bold text-blue-600">
{formatPrice(product.price)}
</span>
<div className="flex items-center gap-1">
<span className="text-yellow-500"></span>
<span className="text-sm text-gray-600">
{product.rating.toFixed(1)} ({product.reviewCount})
</span>
</div>
</div>

<Button
variant="primary"
className="w-full"
disabled={!product.inStock}
onClick={(e) => {
e.stopPropagation();
onAddToCart(product);
}}
>
{product.inStock ? "Add to Cart" : "Out of Stock"}
</Button>
</Card>
))}
</div>
);
}
// libs/customer/feature-products/src/lib/components/ProductFilters.tsx
import { useState } from "react";
import { ProductFilters as Filters } from "@shophub/shared/types";
import { Button } from "@shophub/shared/ui";

interface ProductFiltersProps {
filters: Filters;
onChange: (filters: Filters) => void;
}

export function ProductFilters({ filters, onChange }: ProductFiltersProps) {
const [localFilters, setLocalFilters] = useState<Filters>(filters);

const handleApply = () => {
onChange(localFilters);
};

const handleReset = () => {
const emptyFilters: Filters = {};
setLocalFilters(emptyFilters);
onChange(emptyFilters);
};

return (
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Filters</h3>

{/* Category */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Category</label>
<select
value={localFilters.category || ""}
onChange={(e) =>
setLocalFilters({
...localFilters,
category: e.target.value || undefined,
})
}
className="w-full px-3 py-2 border rounded"
>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
<option value="home">Home & Garden</option>
</select>
</div>

{/* Price Range */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Price Range</label>
<div className="flex gap-2">
<input
type="number"
placeholder="Min"
value={localFilters.minPrice || ""}
onChange={(e) =>
setLocalFilters({
...localFilters,
minPrice: e.target.value ? Number(e.target.value) : undefined,
})
}
className="w-full px-3 py-2 border rounded"
/>
<input
type="number"
placeholder="Max"
value={localFilters.maxPrice || ""}
onChange={(e) =>
setLocalFilters({
...localFilters,
maxPrice: e.target.value ? Number(e.target.value) : undefined,
})
}
className="w-full px-3 py-2 border rounded"
/>
</div>
</div>

{/* In Stock Only */}
<div className="mb-6">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={localFilters.inStockOnly || false}
onChange={(e) =>
setLocalFilters({
...localFilters,
inStockOnly: e.target.checked,
})
}
className="rounded"
/>
<span className="text-sm">In stock only</span>
</label>
</div>

{/* Actions */}
<div className="flex gap-2">
<Button variant="primary" onClick={handleApply} className="flex-1">
Apply
</Button>
<Button variant="secondary" onClick={handleReset} className="flex-1">
Reset
</Button>
</div>
</div>
);
}
// libs/customer/feature-products/src/lib/ProductsFeature.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Product, ProductFilters } from "@shophub/shared/types";
import { useProducts } from "./hooks/use-products";
import { ProductList } from "./components/ProductList";
import { ProductFilters as Filters } from "./components/ProductFilters";
import { Button } from "@shophub/shared/ui";

interface ProductsFeatureProps {
onAddToCart: (product: Product) => void;
}

/**
* Complete product catalog feature
* Includes list, filters, and pagination
*/
export function ProductsFeature({ onAddToCart }: ProductsFeatureProps) {
const navigate = useNavigate();
const [filters, setFilters] = useState<ProductFilters>({});
const { products, loading, error, total, hasMore, loadMore } =
useProducts(filters);

const handleProductClick = (product: Product) => {
navigate(`/products/${product.id}`);
};

if (error) {
return (
<div className="text-center py-12">
<p className="text-red-600 text-lg mb-4">{error}</p>
<Button onClick={() => window.location.reload()}>Try Again</Button>
</div>
);
}

return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Our Products</h1>

<div className="grid grid-cols-4 gap-6">
{/* Filters Sidebar */}
<aside className="col-span-1">
<Filters filters={filters} onChange={setFilters} />
</aside>

{/* Product List */}
<main className="col-span-3">
{/* Results Count */}
<div className="mb-6">
<p className="text-gray-600">
Showing {products.length} of {total} products
</p>
</div>

{/* Loading State */}
{loading && products.length === 0 && (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading products...</p>
</div>
)}

{/* Product Grid */}
{!loading ||
(products.length > 0 && (
<>
<ProductList
products={products}
onProductClick={handleProductClick}
onAddToCart={onAddToCart}
/>

{/* Load More */}
{hasMore && (
<div className="text-center mt-8">
<Button
variant="secondary"
onClick={loadMore}
disabled={loading}
>
{loading ? "Loading..." : "Load More"}
</Button>
</div>
)}
</>
))}
</main>
</div>
</div>
);
}
// libs/customer/feature-products/src/index.ts
/**
* Customer Product Catalog Feature
*
* Complete product browsing experience including:
* - Product listing with pagination
* - Filtering by category, price, availability
* - Product detail views
* - Search functionality
*/

export { ProductsFeature } from "./lib/ProductsFeature";
export { ProductList } from "./lib/components/ProductList";
export { ProductFilters } from "./lib/components/ProductFilters";
export { useProducts } from "./lib/hooks/use-products";

Step 5: Use the Feature in Your App

// apps/portal/src/pages/ProductsPage.tsx
import { ProductsFeature } from "@shophub/customer/feature-products";
import { useCart } from "@shophub/feature/cart";

export function ProductsPage() {
const { addToCart } = useCart();

return <ProductsFeature onAddToCart={addToCart} />;
}

Notice how clean this is:

  • App only imports the feature
  • Feature handles all complexity internally
  • Proper separation of concerns
  • Easy to test each layer independently

Summary: Key Takeaways

Congratulations! You've mastered the art of organizing Nx monorepos. Let's recap the essential patterns you've learned:

Core Concepts

Apps vs Libraries

  • Apps are deployable (dashboard, portal, api, mobile)
  • Libraries are reusable building blocks
  • Apps import from libraries, never from other apps

Five Library Types

  1. Shared - Used by everyone (ui, types, util)
  2. Feature - Complete business features (auth, products, orders)
  3. Data Access - API communication
  4. UI - Presentational components only
  5. Util - Pure utility functions

Domain Organization

  • Group by business domain (customer, admin, shared)
  • Enforce boundaries with project tags
  • Keep related code together

Essential Patterns

Library Naming:

[scope]/[type]-[name]
Examples:
- shared/ui
- customer/feature-products
- admin/feature-orders
- data-access/api-client

Dependency Rules:

Apps → Features → UI/Data-Access → Util
Customer → Customer + Shared
Admin → Admin + Shared
Shared → Shared only

Import Paths:

import { Product } from "@shophub/shared/types";
import { Button } from "@shophub/shared/ui";
import { formatPrice } from "@shophub/shared/util";
import { ProductsFeature } from "@shophub/customer/feature-products";

Common Mistakes to Avoid

❌ Creating too many tiny libraries ❌ Not planning for growth from the start ❌ Mixing concerns in one library ❌ App-to-app dependencies ❌ Circular dependencies between libraries ❌ UI components with business logic

Best Practices

✅ Start with organized structure ✅ Use project tags from day one ✅ Enforce boundaries with ESLint rules ✅ Keep libraries focused (one responsibility) ✅ Use path mappings for clean imports ✅ Visualize dependencies regularly (nx graph) ✅ Export only public API from libraries


What's Next?

You now understand how to organize an Nx workspace like a professional! You know when to create apps vs libraries, how to structure code by domain, and how to enforce architectural rules.

Continue learning:

  • Deep dive into creating effective shared libraries
  • Advanced patterns for code reuse
  • Testing strategies for libraries
  • Publishing and versioning

Check Your Understanding

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

Quick Quiz

1. When should code be in an app vs a library?

Show Answer

App:

  • Code that is deployed and runs independently
  • Has a main entry point (main.ts, index.html)
  • Users access it directly
  • Examples: dashboard, portal, api, mobile

Library:

  • Reusable code shared between apps
  • Cannot run on its own
  • Imported by apps or other libraries
  • Examples: shared components, utilities, types, features

Simple test: "Will this be deployed and accessed by users?"

  • YES → App
  • NO → Library

2. What's wrong with this import?

// apps/portal/src/pages/Home.tsx
import { formatPrice } from "../../../dashboard/src/utils/price";
Show Answer

Multiple problems:

  1. App importing from app - Portal shouldn't depend on Dashboard
  2. Tight coupling - Can't deploy Portal without Dashboard code
  3. Relative path hell - Fragile, breaks when files move

Correct approach:

// Extract to library
// libs/shared/util/src/lib/format-price.ts
export function formatPrice(price: number): string {
return `${price.toFixed(2)}`;
}

// apps/portal/src/pages/Home.tsx
import { formatPrice } from "@shophub/shared/util";

Why this is better:

  • Both apps depend on library, not each other
  • Clean import path
  • Easy to refactor
  • Independent deployment

3. Which library type should this code be in?

export function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
Show Answer

Answer: UI Library (libs/shared/ui)

Why:

  • Presentational component
  • No business logic
  • Reusable across apps
  • Receives data via props
  • Pure UI functionality

Full path:

libs/shared/ui/src/lib/button/Button.tsx

Import:

import { Button } from "@shophub/shared/ui";

4. What's wrong with this library structure?

libs/shared/everything/
├── Button.tsx (UI component)
├── formatDate.ts (Utility)
├── Product.ts (Type)
├── getProducts.ts (API call)
└── useAuth.ts (Business logic)
Show Answer

Problem: Mixed concerns - This library does everything!

Why it's bad:

  • No clear responsibility
  • Hard to understand purpose
  • Poor tree-shaking
  • Can't enforce dependency rules
  • Difficult to test

Correct structure:

libs/shared/ui/
└── Button.tsx

libs/shared/util/
└── formatDate.ts

libs/shared/types/
└── Product.ts

libs/data-access/products-api/
└── getProducts.ts

libs/feature/auth/
└── useAuth.ts

Rule: One library = One responsibility

Hands-On Exercise

Challenge: Organize this messy monorepo

Current structure (bad):

my-workspace/
├── customer-app/
├── admin-app/
├── api-app/
├── button-component/
├── card-component/
├── auth-logic/
├── product-stuff/
└── utils/

Your task:

  1. Reorganize into proper apps/ and libs/ structure
  2. Use appropriate library types
  3. Add domain scoping
  4. Name following conventions

Solution:

Show Answer
my-workspace/
├── apps/
│ ├── portal/ ← customer-app (renamed)
│ ├── dashboard/ ← admin-app (renamed)
│ └── api/ ← api-app
├── libs/
│ ├── shared/
│ │ ├── ui/ ← button + card components
│ │ ├── types/ ← Shared types
│ │ └── util/ ← utils
│ ├── feature/
│ │ └── auth/ ← auth-logic
│ ├── customer/
│ │ └── feature-products/ ← product-stuff (customer view)
│ ├── admin/
│ │ └── feature-products/ ← product-stuff (admin view)
│ └── data-access/
│ └── api-client/ ← API communication
└── tools/

Key improvements:

  • Clear app vs library separation
  • Libraries organized by type and domain
  • Consistent naming (feature-*, scope/type-name)
  • Proper scoping (customer vs admin)
  • Foundation for scaling

Version Information

Tested with:

  • Nx: v19.0.0+
  • Node.js: v18.x, v20.x, v22.x
  • pnpm: v8.x, v9.x
  • React: v18.3.1
  • TypeScript: v5.4.5

Platform support:

  • ✅ macOS (Intel & Apple Silicon)
  • ✅ Windows 10/11
  • ✅ Linux (Ubuntu 20.04+)
  • ✅ WSL2

Note: The organizational patterns in this article apply to all Nx versions and can be adapted to any framework (React, Angular, Vue, etc.).