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
Productentity in a B2B e-commerce platform. Consider fields forproductId,name,description,SKU,category,price (array for tiered pricing),stockQuantity,supplierInfo,images,customizableOptions, andavailability (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:
- 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 initialnpm installfor 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 namedshop-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 marksProductServiceas 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 anObservableofProduct[].of()from RxJS immediately emits theproductsarray 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 importsCommonModuleto use directives like*ngForand*ngIf.products$!: Observable<Product[]>: We use the$suffix to denote an Observable. The!is a definite assignment assertion, telling TypeScript thatproducts$will definitely be assigned inngOnInit.constructor(private productService: ProductService): Angular’s dependency injection system provides an instance ofProductService.ngOnInit(): This lifecycle hook is where we fetch the products when the component initializes. We assign the observable directly toproducts$.
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: Theasyncpipe subscribes toproducts$and automatically unwraps the emitted value.as products: Assigns the emitted value (the array of products) to a local template variableproducts.else loadingOrError: Ifproducts$hasn’t emitted yet (or isnull/undefined), theloadingOrErrortemplate is shown.
*ngFor="let product of products": Iterates over theproductsarray 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/productsURL path to ourProductListComponent.
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>© 2026 B2B E-commerce Solution</p>
</footer>
routerLink="/products": A directive that creates a link to the/productsroute.routerLinkActive="active": Adds the CSS classactiveto 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:
- Display Minimum Order Quantity: Add a line to each
product-cardinproduct-list.component.htmlthat showsMin. Order: {{ product.minOrderQuantity }}. - Display Available Regions: Add a line showing
Regions: {{ product.availableRegions.join(', ') }}. - Implement a View Details Click: Modify the “View Details” button to have a click handler
(click)="viewProductDetails(product.id)". - Add
viewProductDetailsMethod: Inproduct-list.component.ts, add a methodviewProductDetails(productId: string)that simply logs theproductIdto 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
NullInjectorErrorforProductService:- Pitfall: Forgetting
providedIn: 'root'in your service’s@Injectabledecorator, or not importing the service correctly in a standalone component (thoughprovidedIn: 'root'handles this for app-wide services). - Troubleshooting: Double-check
product.service.tsto ensure@Injectable({ providedIn: 'root' })is present.
- Pitfall: Forgetting
Error: NG0300or Template Parse Errors:- Pitfall: When using standalone components, forgetting to import
CommonModulefor directives like*ngFor,*ngIf,[ngClass], etc., orRouterModuleforrouterLink,router-outlet. - Troubleshooting: Review the
importsarray in your standalone component’s@Componentdecorator. EnsureCommonModuleis listed if you’re using common Angular directives, andRouterModuleif you’re using routing directives or outlets in that component’s template.
- Pitfall: When using standalone components, forgetting to import
- No Products Displayed / “Loading products…” never disappears:
- Pitfall: The
products$Observable might not be emitting data, or theasyncpipe isn’t working as expected. This could be due to a bug ingetProducts()orproductsarray being empty. - Troubleshooting:
- Open your browser’s developer console. Are there any errors?
- Add
console.log(this.products)insideProductService’sgetProducts()to confirm data exists. - Add
console.log(products)inside the*ngIfblock in your component’s template (e.g.,<div *ngIf="products$ | async as products; else loadingOrError" class="product-grid">{{ console.log(products) }} ...). This confirms if theasyncpipe is unwrapping data. - Ensure the mock data in
ProductServiceis correctly formatted according toProductinterface.
- Pitfall: The
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
Productinterface tailored for B2B needs, including tiered pricing and minimum order quantities. - We created a
ProductServiceto simulate backend data fetching using RxJSObservableandof(). - We built the
ProductListComponentto display our product catalog, leveraging theasyncpipe 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
- Angular Standalone Components Documentation
- Angular CLI Documentation
- RxJS
of()Operator - Angular
asyncPipe Documentation - Angular Router Documentation
- Node.js Official Website
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.