Introduction: Architecting for Enterprise E-commerce

Welcome back, future Angular architects! In our previous project, we laid the groundwork for complex enterprise applications. Now, we’re diving into a crucial domain for many businesses: a B2B E-commerce Platform. This isn’t your typical consumer-facing online store; B2B e-commerce often involves intricate pricing, customer-specific catalogs, order approvals, and robust account management.

In this chapter, we’ll begin building a core module for such a platform: the Product Catalog and Search Module. This will give us a chance to apply advanced Angular concepts like scalable component architecture, efficient data fetching, and intelligent filtering. We’ll leverage modern Angular features, including standalone components, and explore how AI can assist in accelerating our development workflow, from data modeling to component generation.

Before we begin, ensure you’re comfortable with:

  • Angular CLI and project setup.
  • Components, directives, and services.
  • Basic routing.
  • Observables and RxJS fundamentals.

Ready to build something truly robust? Let’s get started!

Understanding B2B E-commerce Modularity

Building an e-commerce platform for businesses requires a different mindset than building one for consumers. 📌 Key Idea: B2B applications prioritize scalability, complex business logic, and integration, often requiring a highly modular and maintainable codebase.

Why Modularity Matters in B2B

Imagine an e-commerce platform that needs to cater to different client tiers, integrate with various ERP systems, and handle custom product configurations. A monolithic approach quickly becomes unmanageable. Modularity allows us to:

  • Isolate Features: Each core feature (e.g., Product Catalog, Order Management, User Accounts) can be developed and deployed somewhat independently.
  • Improve Team Collaboration: Different teams can work on separate modules without constant merge conflicts.
  • Enhance Scalability: Specific modules can be scaled or optimized without affecting the entire application.
  • Simplify Maintenance and Upgrades: Updates or bug fixes to one module are less likely to break others.

For our Product Catalog and Search Module, we’ll structure it as a feature area within our larger Angular application, primarily using standalone components to promote self-contained and tree-shakable units.

AI’s Role in Accelerating Modular Design

Before even writing code, AI tools can be invaluable. ⚡ Quick Note: AI can help you brainstorm data models, suggest API endpoints, and even scaffold basic component structures based on descriptions.

For instance, you could prompt an AI like Claude or Copilot:

“Design a JSON structure for a Product entity in a B2B e-commerce platform. Consider fields for productId, name, description, SKU, category, price (array for tiered pricing), stockQuantity, supplierInfo, images, customizableOptions, and availability (region-specific). Also, suggest a TypeScript interface.”

This helps kickstart your data modeling, ensuring you consider key attributes upfront for a B2B context.

The Product Catalog Module: A High-Level View

Our module will focus on displaying products, handling search, and enabling basic filtering. It will interact with a “backend” (initially simulated) to fetch product data.

Here’s a simplified view of how our module components will interact:

flowchart LR CatalogModule[Product Catalog Module] --> ProductList[Product List Component] ProductList --> ProductCard[Product Card Component] CatalogModule --> SearchFilter[Search Filter Component] SearchFilter --> ProductList ProductList -->|Select Product| ProductDetail[Product Detail Component]
  • Product Catalog Module: Our conceptual grouping of standalone components, services, and routing for this feature.
  • Search Filter Component: Allows users to input search terms and apply filters (e.g., by category, price range).
  • Product List Component: Displays a collection of products based on applied filters and search terms.
  • Product Card Component: A reusable presentational component for displaying individual product information concisely.
  • Product Detail Component: Shows comprehensive information about a single product.

This separation of concerns makes our module robust and reusable.

Step-by-Step Implementation: Product Catalog Basics

Let’s get our hands dirty and start building this module!

Step 1: Initialize the Angular Project and E-commerce Application

First, ensure you have Node.js (v20.x or later, as of 2026-05-06) and the Angular CLI installed. We’ll use the latest stable Angular CLI, which for May 2026, we anticipate to be around Angular CLI v21.x (or potentially v22.x, depending on exact release schedules; always verify the latest stable release via ng version or the official Angular documentation).

# Verify Node.js version
node --version
# Expected: v20.x.x or higher

# Install/update Angular CLI globally
npm install -g @angular/cli@latest
# Verify Angular CLI version
ng version
# Expected: Angular CLI: 21.x.x (or latest stable available)
# Node: 20.x.x
# Package Manager: npm 10.x.x

Now, let’s create a new Angular workspace and an application within it. We’ll start with a basic shell application.

# Create a new workspace
ng new b2b-ecommerce-workspace --no-create-application --skip-install --collection=@schematics/angular

# Navigate into the workspace
cd b2b-ecommerce-workspace

# Add a new application named 'shop-app' using standalone components and SCSS
ng generate application shop-app --standalone --style=scss --routing --skip-install
  • ng new b2b-ecommerce-workspace --no-create-application: Creates a new Angular workspace but doesn’t immediately create an application, allowing us to add it separately.
  • --skip-install: Skips the initial npm install for the workspace, as we’ll do it after adding the application.
  • --collection=@schematics/angular: Specifies the default schematics to use.
  • ng generate application shop-app --standalone --style=scss --routing: Generates a new application named shop-app.
    • --standalone: Crucially, this sets up the application to use standalone components, aligning with modern Angular practices.
    • --style=scss: Configures SCSS for styling.
    • --routing: Sets up a basic routing module.

Finally, install all dependencies:

npm install

Start the development server to verify everything is working:

ng serve --open

You should see the default Angular welcome page.

Step 2: Define the Product Interface

Based on our AI-assisted brainstorming, let’s create a TypeScript interface for our products. This ensures type safety throughout our application.

Create a new folder src/app/shared/models and inside it, a file product.model.ts.

// src/app/shared/models/product.model.ts
export interface Product {
  id: string;
  name: string;
  description: string;
  sku: string;
  category: string;
  priceTiers: PriceTier[]; // Array for different quantity-based pricing
  stockQuantity: number;
  supplierInfo?: string; // Optional supplier details
  imageUrl: string; // Simplified for this project
  customizableOptions?: string[]; // E.g., colors, sizes
  minOrderQuantity: number; // For B2B, a minimum order is common
  availableRegions: string[];
}

export interface PriceTier {
  quantity: number; // Minimum quantity for this tier
  unitPrice: number;
}
  • export interface Product: Defines the structure of our product data.
  • priceTiers: PriceTier[]: Instead of a single price, B2B often has tiered pricing based on quantity.
  • minOrderQuantity: A common B2B requirement.

Step 3: Create a Product Data Service (Mock API)

We need a way to fetch product data. For now, we’ll create a mock service that returns a hardcoded array of products. This mimics an API call without needing a real backend.

Generate a new service:

ng generate service src/app/products/product

This will create src/app/products/product.service.ts and src/app/products/product.service.spec.ts.

Now, modify product.service.ts to provide mock product data:

// src/app/products/product.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs'; // 'of' creates an observable that emits values
import { Product } from '../shared/models/product.model';

@Injectable({
  providedIn: 'root' // Makes this service a singleton, available throughout the app
})
export class ProductService {

  private products: Product[] = [
    {
      id: 'prod-001',
      name: 'Industrial Grade Bolt Set',
      description: 'High-strength steel bolts suitable for heavy machinery.',
      sku: 'IG-BOLT-001',
      category: 'Fasteners',
      priceTiers: [{ quantity: 1, unitPrice: 10.99 }, { quantity: 100, unitPrice: 9.99 }, { quantity: 1000, unitPrice: 8.50 }],
      stockQuantity: 5000,
      supplierInfo: 'Acme Supplies Inc.',
      imageUrl: 'https://via.placeholder.com/150/0000FF/FFFFFF?text=BoltSet',
      minOrderQuantity: 10,
      availableRegions: ['North America', 'Europe']
    },
    {
      id: 'prod-002',
      name: 'Heavy Duty Caster Wheel',
      description: 'Swivel caster wheel with 500kg load capacity.',
      sku: 'HD-CASTER-002',
      category: 'Hardware',
      priceTiers: [{ quantity: 1, unitPrice: 25.00 }, { quantity: 50, unitPrice: 22.50 }],
      stockQuantity: 1200,
      supplierInfo: 'Global Wheels Co.',
      imageUrl: 'https://via.placeholder.com/150/FF0000/FFFFFF?text=CasterWheel',
      customizableOptions: ['Wheel Material', 'Brake Type'],
      minOrderQuantity: 1,
      availableRegions: ['Global']
    },
    {
        id: 'prod-003',
        name: 'Precision Ball Bearings (Box of 100)',
        description: 'High-tolerance ball bearings for industrial applications.',
        sku: 'PB-BEARING-003',
        category: 'Components',
        priceTiers: [{ quantity: 1, unitPrice: 150.00 }, { quantity: 10, unitPrice: 140.00 }],
        stockQuantity: 800,
        supplierInfo: 'Bearing Tech Ltd.',
        imageUrl: 'https://via.placeholder.com/150/00FF00/FFFFFF?text=Bearings',
        minOrderQuantity: 1,
        availableRegions: ['North America', 'Asia']
    },
    {
      id: 'prod-004',
      name: 'High-Efficiency Electric Motor',
      description: 'Compact 5HP electric motor for various industrial uses.',
      sku: 'HE-MOTOR-004',
      category: 'Motors',
      priceTiers: [{ quantity: 1, unitPrice: 750.00 }],
      stockQuantity: 50,
      supplierInfo: 'Electro動力 Corp.',
      imageUrl: 'https://via.placeholder.com/150/FFFF00/000000?text=ElectricMotor',
      customizableOptions: ['Voltage', 'Phase'],
      minOrderQuantity: 1,
      availableRegions: ['Europe', 'Asia']
    }
  ];

  constructor() { }

  getProducts(): Observable<Product[]> {
    // Simulate an async API call with a short delay
    return of(this.products);
  }

  getProductById(id: string): Observable<Product | undefined> {
    return of(this.products.find(product => product.id === id));
  }
}
  • @Injectable({ providedIn: 'root' }): This decorator marks ProductService as an injectable service and ensures it’s a singleton available throughout the application.
  • private products: Product[]: An array holding our mock product data.
  • getProducts(): Observable<Product[]>: Returns an Observable of Product[]. of() from RxJS immediately emits the products array and then completes, simulating a quick API response.
  • getProductById(id: string): Observable<Product | undefined>: Fetches a single product by its ID.

Step 4: Create the Product List Standalone Component

Now, let’s create the component that will display our products. This will be a standalone component.

ng generate component src/app/products/product-list --standalone --skip-tests
  • --standalone: Generates the component as a standalone component, meaning it manages its own imports.
  • --skip-tests: For brevity in this lesson, we’ll skip generating test files. In a real project, you’d always include tests!

Open src/app/products/product-list/product-list.component.ts and modify it:

// src/app/products/product-list/product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; // Needed for NgFor, NgIf
import { ProductService } from '../product.service';
import { Product } from '../../shared/models/product.model';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-product-list',
  standalone: true, // This component is standalone
  imports: [CommonModule], // We need CommonModule for structural directives like *ngFor
  templateUrl: './product-list.component.html',
  styleUrl: './product-list.component.scss'
})
export class ProductListComponent implements OnInit {
  products$!: Observable<Product[]>; // '!' tells TypeScript it will be initialized

  constructor(private productService: ProductService) { }

  ngOnInit(): void {
    this.products$ = this.productService.getProducts();
  }
}
  • standalone: true: Confirms this is a standalone component.
  • imports: [CommonModule]: Since it’s standalone, it explicitly imports CommonModule to use directives like *ngFor and *ngIf.
  • products$!: Observable<Product[]> : We use the $ suffix to denote an Observable. The ! is a definite assignment assertion, telling TypeScript that products$ will definitely be assigned in ngOnInit.
  • constructor(private productService: ProductService): Angular’s dependency injection system provides an instance of ProductService.
  • ngOnInit(): This lifecycle hook is where we fetch the products when the component initializes. We assign the observable directly to products$.

Now, let’s update the template src/app/products/product-list/product-list.component.html:

<!-- src/app/products/product-list/product-list.component.html -->
<div class="product-list-container">
  <h2>Product Catalog</h2>

  <!-- The async pipe unwraps the Observable and subscribes/unsubscribes automatically -->
  <div *ngIf="products$ | async as products; else loadingOrError" class="product-grid">
    <div *ngFor="let product of products" class="product-card">
      <img [src]="product.imageUrl" [alt]="product.name">
      <h3>{{ product.name }}</h3>
      <p class="category">{{ product.category }}</p>
      <p class="price">From ${{ product.priceTiers[0].unitPrice.toFixed(2) }}</p>
      <button>View Details</button>
    </div>
  </div>

  <ng-template #loadingOrError>
    <p>Loading products...</p>
  </ng-template>
</div>
  • *ngIf="products$ | async as products; else loadingOrError": This is a powerful pattern.
    • products$ | async: The async pipe subscribes to products$ and automatically unwraps the emitted value.
    • as products: Assigns the emitted value (the array of products) to a local template variable products.
    • else loadingOrError: If products$ hasn’t emitted yet (or is null/undefined), the loadingOrError template is shown.
  • *ngFor="let product of products": Iterates over the products array to display each product.
  • [src]="product.imageUrl": Property binding for the image source.
  • {{ product.name }}, {{ product.category }}, etc.: Interpolation to display product properties.
  • product.priceTiers[0].unitPrice.toFixed(2): We’re showing the lowest tier price for display.

Finally, add some basic styling to src/app/products/product-list/product-list.component.scss:

/* src/app/products/product-list/product-list.component.scss */
.product-list-container {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;

  h2 {
    text-align: center;
    margin-bottom: 30px;
    color: #333;
  }
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
}

.product-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 15px;
  text-align: center;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  background-color: #fff;
  display: flex;
  flex-direction: column;
  justify-content: space-between;

  img {
    max-width: 100%;
    height: 150px;
    object-fit: contain;
    margin-bottom: 10px;
    border-bottom: 1px solid #eee;
    padding-bottom: 10px;
  }

  h3 {
    font-size: 1.2em;
    margin: 10px 0;
    color: #222;
  }

  .category {
    font-size: 0.9em;
    color: #666;
    margin-bottom: 5px;
  }

  .price {
    font-size: 1.1em;
    font-weight: bold;
    color: #007bff;
    margin-top: 10px;
  }

  button {
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 5px;
    padding: 10px 15px;
    cursor: pointer;
    margin-top: 15px;
    font-size: 0.9em;

    &:hover {
      background-color: #0056b3;
    }
  }
}

Step 5: Add Routing for the Product List

Now, let’s make our ProductListComponent accessible via a route.

Open src/app/app.routes.ts. This is where our application’s routes are defined.

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { ProductListComponent } from './products/product-list/product-list.component';

export const routes: Routes = [
    { path: '', redirectTo: 'products', pathMatch: 'full' }, // Redirect root to products
    { path: 'products', component: ProductListComponent } // Route for product list
];
  • import { ProductListComponent } from './products/product-list/product-list.component';: Imports our standalone component.
  • { path: '', redirectTo: 'products', pathMatch: 'full' }: When the user navigates to the root URL (/), it will automatically redirect to /products. pathMatch: 'full' ensures the entire path must match.
  • { path: 'products', component: ProductListComponent }: Maps the /products URL path to our ProductListComponent.

Finally, we need to ensure our app.component.html has a <router-outlet> where the routed components will be displayed.

Open src/app/app.component.html and replace its content with this simplified structure:

<!-- src/app/app.component.html -->
<header>
  <h1>B2B E-commerce Portal</h1>
  <nav>
    <a routerLink="/products" routerLinkActive="active">Product Catalog</a>
    <!-- Other navigation links will go here -->
  </nav>
</header>

<main>
  <router-outlet></router-outlet> <!-- This is where our components will be rendered -->
</main>

<footer>
  <p>&copy; 2026 B2B E-commerce Solution</p>
</footer>
  • routerLink="/products": A directive that creates a link to the /products route.
  • routerLinkActive="active": Adds the CSS class active to the link when the current route is /products.
  • <router-outlet>: This is a placeholder that Angular uses to dynamically load and display components based on the current route.

Add some basic global styling to src/styles.scss (or src/app/app.component.scss if you prefer component-scoped styles for the layout):

/* src/styles.scss */
body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  margin: 0;
  background-color: #f8f9fa;
  color: #333;
}

header {
  background-color: #212529;
  color: white;
  padding: 15px 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;

  h1 {
    margin: 0;
    font-size: 1.5em;
  }

  nav a {
    color: white;
    text-decoration: none;
    margin-left: 20px;
    padding: 5px 10px;
    border-radius: 4px;

    &:hover {
      background-color: #495057;
    }

    &.active {
      background-color: #007bff;
    }
  }
}

main {
  padding: 20px;
  min-height: calc(100vh - 120px); /* Adjust based on header/footer height */
}

footer {
  background-color: #e9ecef;
  color: #6c757d;
  text-align: center;
  padding: 15px 20px;
  margin-top: 30px;
}

Now, save all files and make sure your ng serve is running. Navigate to http://localhost:4200 (or whatever port your ng serve uses). You should now see the header, a “Product Catalog” link, and below it, our list of mock products!

Mini-Challenge: Enhance Product Display

Your challenge is to improve the product card by adding more relevant B2B information and allowing the “View Details” button to log the product ID.

Challenge:

  1. Display Minimum Order Quantity: Add a line to each product-card in product-list.component.html that shows Min. Order: {{ product.minOrderQuantity }}.
  2. Display Available Regions: Add a line showing Regions: {{ product.availableRegions.join(', ') }}.
  3. Implement a View Details Click: Modify the “View Details” button to have a click handler (click)="viewProductDetails(product.id)".
  4. Add viewProductDetails Method: In product-list.component.ts, add a method viewProductDetails(productId: string) that simply logs the productId to the console for now.

Hint:

  • Remember to use interpolation {{ }} for displaying data.
  • The join(', ') method on an array is useful for displaying array elements as a comma-separated string.

What to observe/learn: You’ll practice iterating through product data, formatting it for display, and implementing basic event handling in standalone components. This sets the stage for more complex interactions like navigating to a product detail page.

Common Pitfalls & Troubleshooting

  1. NullInjectorError for ProductService:
    • Pitfall: Forgetting providedIn: 'root' in your service’s @Injectable decorator, or not importing the service correctly in a standalone component (though providedIn: 'root' handles this for app-wide services).
    • Troubleshooting: Double-check product.service.ts to ensure @Injectable({ providedIn: 'root' }) is present.
  2. Error: NG0300 or Template Parse Errors:
    • Pitfall: When using standalone components, forgetting to import CommonModule for directives like *ngFor, *ngIf, [ngClass], etc., or RouterModule for routerLink, router-outlet.
    • Troubleshooting: Review the imports array in your standalone component’s @Component decorator. Ensure CommonModule is listed if you’re using common Angular directives, and RouterModule if you’re using routing directives or outlets in that component’s template.
  3. No Products Displayed / “Loading products…” never disappears:
    • Pitfall: The products$ Observable might not be emitting data, or the async pipe isn’t working as expected. This could be due to a bug in getProducts() or products array being empty.
    • Troubleshooting:
      • Open your browser’s developer console. Are there any errors?
      • Add console.log(this.products) inside ProductService’s getProducts() to confirm data exists.
      • Add console.log(products) inside the *ngIf block in your component’s template (e.g., <div *ngIf="products$ | async as products; else loadingOrError" class="product-grid">{{ console.log(products) }} ...). This confirms if the async pipe is unwrapping data.
      • Ensure the mock data in ProductService is correctly formatted according to Product interface.

Summary: Building a Solid Foundation

In this chapter, we’ve taken significant steps towards building a robust B2B E-commerce platform module:

  • We initiated a new Angular application, prioritizing standalone components for modern, modular development.
  • We defined a Product interface tailored for B2B needs, including tiered pricing and minimum order quantities.
  • We created a ProductService to simulate backend data fetching using RxJS Observable and of().
  • We built the ProductListComponent to display our product catalog, leveraging the async pipe for efficient data binding.
  • We configured routing to make our product list accessible via a clean URL.
  • We briefly touched upon how AI tools can streamline initial design and data modeling phases.

You’ve now got a functional core for a product catalog! This modular foundation is critical for enterprise-grade applications.

What’s Next?

In the next chapter, we’ll expand on this foundation by:

  • Implementing the Search and Filter Component to dynamically update the product list.
  • Creating the Product Detail Component to view individual product information.
  • Exploring more advanced component communication patterns.
  • Diving deeper into state management for selected products or a shopping cart.

Keep experimenting with what we’ve built. The more you play with the code, the deeper your understanding will become.


References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.