Skip to main content
Enterprise Capstone

Enterprise Capstone Project

Estimated Time: 20+ hours | Difficulty: Advanced | Prerequisites: All Previous Modules
Build a complete enterprise-grade e-commerce platform that implements all concepts covered in this course. This capstone project simulates real-world development scenarios.
┌─────────────────────────────────────────────────────────────────────────┐
│                    E-Commerce Platform Architecture                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                     Frontend (Angular 17+)                       │   │
│   │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐   │   │
│   │  │  Auth   │ │Products │ │  Cart   │ │ Orders  │ │  Admin  │   │   │
│   │  └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘   │   │
│   │       └───────────┴───────────┼───────────┴───────────┘         │   │
│   │                               │                                  │   │
│   │                    ┌──────────┴──────────┐                      │   │
│   │                    │   NgRx Signal Store  │                      │   │
│   │                    └──────────┬──────────┘                      │   │
│   │                               │                                  │   │
│   │                    ┌──────────┴──────────┐                      │   │
│   │                    │    HTTP Services    │                      │   │
│   │                    └──────────┬──────────┘                      │   │
│   └───────────────────────────────┼─────────────────────────────────┘   │
│                                   │                                      │
│   ┌───────────────────────────────┼─────────────────────────────────┐   │
│   │                     Backend API (REST)                           │   │
│   │                               │                                  │   │
│   │   ┌──────────┐  ┌──────────┐  │  ┌──────────┐  ┌──────────┐     │   │
│   │   │   Auth   │  │ Products │◄─┴─►│  Orders  │  │  Users   │     │   │
│   │   └──────────┘  └──────────┘     └──────────┘  └──────────┘     │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Project Requirements

Functional Requirements

Authentication

  • User registration/login
  • JWT token management
  • Role-based access (User/Admin)
  • Password reset flow
  • Social login (optional)

Product Catalog

  • Product listing with filters
  • Search functionality
  • Category navigation
  • Product details page
  • Image gallery

Shopping Cart

  • Add/remove items
  • Quantity updates
  • Price calculations
  • Persistent cart
  • Guest checkout

Checkout & Orders

  • Multi-step checkout
  • Address management
  • Payment integration
  • Order confirmation
  • Order history

User Dashboard

  • Profile management
  • Order tracking
  • Wishlist
  • Reviews & ratings
  • Notifications

Admin Panel

  • Product CRUD
  • Order management
  • User management
  • Analytics dashboard
  • Inventory control

Non-Functional Requirements

  • Performance: First Contentful Paint < 1.5s, Lighthouse score > 90
  • Accessibility: WCAG 2.1 AA compliant
  • Security: OWASP top 10 protections
  • Testing: 80%+ code coverage
  • SEO: Server-side rendering for public pages
  • i18n: Multi-language support

Project Setup

# Create new Angular project
ng new ecommerce-platform --style=scss --ssr

# Add dependencies
npm install @ngrx/signals @angular/cdk
npm install -D @playwright/test cypress

# Generate project structure
ng g @angular/core:standalone-migration --path=./src/app

# Create feature structure
mkdir -p src/app/{core,shared,features,layouts}
mkdir -p src/app/features/{auth,products,cart,checkout,orders,admin}

Application Configuration

// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(
      routes,
      withComponentInputBinding(),
      withViewTransitions()
    ),
    provideHttpClient(
      withFetch(),
      withInterceptors([
        authInterceptor,
        errorInterceptor,
        loadingInterceptor
      ])
    ),
    provideClientHydration(withEventReplay()),
    provideAnimationsAsync()
  ]
};

Core Module Implementation

Authentication Store

// features/auth/auth.store.ts
import { signalStore, withState, withMethods, withComputed } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe, switchMap, tap } from 'rxjs';

interface AuthState {
  user: User | null;
  token: string | null;
  loading: boolean;
  error: string | null;
}

const initialState: AuthState = {
  user: null,
  token: null,
  loading: false,
  error: null
};

export const AuthStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed((store) => ({
    isAuthenticated: computed(() => !!store.token()),
    isAdmin: computed(() => store.user()?.role === 'admin'),
    userName: computed(() => store.user()?.name ?? 'Guest')
  })),
  withMethods((store, authService = inject(AuthService)) => ({
    login: rxMethod<LoginCredentials>(
      pipe(
        tap(() => patchState(store, { loading: true, error: null })),
        switchMap((credentials) =>
          authService.login(credentials).pipe(
            tapResponse({
              next: (response) => {
                patchState(store, {
                  user: response.user,
                  token: response.token,
                  loading: false
                });
                localStorage.setItem('token', response.token);
              },
              error: (error: Error) => {
                patchState(store, { error: error.message, loading: false });
              }
            })
          )
        )
      )
    ),
    
    logout() {
      patchState(store, initialState);
      localStorage.removeItem('token');
    },
    
    initFromStorage() {
      const token = localStorage.getItem('token');
      if (token) {
        authService.validateToken(token).subscribe({
          next: (user) => patchState(store, { user, token }),
          error: () => this.logout()
        });
      }
    }
  }))
);

Cart Store

// features/cart/cart.store.ts
export const CartStore = signalStore(
  { providedIn: 'root' },
  withState<CartState>({
    items: [],
    loading: false
  }),
  withComputed((store) => ({
    itemCount: computed(() => 
      store.items().reduce((sum, item) => sum + item.quantity, 0)
    ),
    subtotal: computed(() =>
      store.items().reduce((sum, item) => 
        sum + item.product.price * item.quantity, 0
      )
    ),
    tax: computed(() => store.subtotal() * 0.1),
    total: computed(() => store.subtotal() + store.tax())
  })),
  withMethods((store) => ({
    addItem(product: Product, quantity = 1) {
      patchState(store, (state) => {
        const existingIndex = state.items.findIndex(
          item => item.product.id === product.id
        );
        
        if (existingIndex >= 0) {
          const items = [...state.items];
          items[existingIndex] = {
            ...items[existingIndex],
            quantity: items[existingIndex].quantity + quantity
          };
          return { items };
        }
        
        return {
          items: [...state.items, { product, quantity }]
        };
      });
    },
    
    updateQuantity(productId: string, quantity: number) {
      patchState(store, (state) => ({
        items: state.items.map(item =>
          item.product.id === productId
            ? { ...item, quantity: Math.max(0, quantity) }
            : item
        ).filter(item => item.quantity > 0)
      }));
    },
    
    removeItem(productId: string) {
      patchState(store, (state) => ({
        items: state.items.filter(item => item.product.id !== productId)
      }));
    },
    
    clearCart() {
      patchState(store, { items: [] });
    }
  }))
);

Product Feature

Product List Component

// features/products/product-list.component.ts
@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [
    CommonModule,
    RouterLink,
    ProductCardComponent,
    ProductFiltersComponent,
    PaginationComponent,
    SkeletonLoaderComponent
  ],
  template: `
    <div class="products-page">
      <aside class="filters-sidebar">
        <app-product-filters
          [categories]="categories()"
          [priceRange]="priceRange()"
          [filters]="filters()"
          (filtersChange)="onFiltersChange($event)"
        />
      </aside>
      
      <main class="products-main">
        <header class="products-header">
          <h1>{{ categoryTitle() }}</h1>
          <div class="sort-controls">
            <select [value]="sortBy()" (change)="onSortChange($event)">
              <option value="newest">Newest</option>
              <option value="price-low">Price: Low to High</option>
              <option value="price-high">Price: High to Low</option>
              <option value="rating">Top Rated</option>
            </select>
          </div>
        </header>
        
        @if (loading()) {
          <div class="products-grid">
            @for (i of skeletonItems; track i) {
              <app-skeleton-loader type="product-card" />
            }
          </div>
        } @else if (products().length === 0) {
          <app-empty-state
            icon="search"
            title="No products found"
            message="Try adjusting your filters"
          />
        } @else {
          <div class="products-grid">
            @for (product of products(); track product.id) {
              <app-product-card
                [product]="product"
                (addToCart)="onAddToCart($event)"
                (addToWishlist)="onAddToWishlist($event)"
              />
            }
          </div>
          
          <app-pagination
            [currentPage]="currentPage()"
            [totalPages]="totalPages()"
            (pageChange)="onPageChange($event)"
          />
        }
      </main>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
  private store = inject(ProductStore);
  private cartStore = inject(CartStore);
  private route = inject(ActivatedRoute);
  
  // From store
  products = this.store.products;
  loading = this.store.loading;
  categories = this.store.categories;
  totalPages = this.store.totalPages;
  
  // Local state
  filters = signal<ProductFilters>({});
  sortBy = signal<string>('newest');
  currentPage = signal(1);
  
  // Computed
  categoryTitle = computed(() => {
    const category = this.filters().category;
    return category ?? 'All Products';
  });
  
  priceRange = computed(() => ({
    min: 0,
    max: Math.max(...this.products().map(p => p.price))
  }));
  
  skeletonItems = Array(8).fill(0);
  
  constructor() {
    // React to route params
    effect(() => {
      const category = this.route.snapshot.params['category'];
      if (category) {
        this.filters.update(f => ({ ...f, category }));
      }
    });
    
    // Load products when filters change
    effect(() => {
      this.store.loadProducts({
        ...this.filters(),
        sort: this.sortBy(),
        page: this.currentPage()
      });
    });
  }
  
  onFiltersChange(filters: ProductFilters) {
    this.filters.set(filters);
    this.currentPage.set(1);
  }
  
  onSortChange(event: Event) {
    this.sortBy.set((event.target as HTMLSelectElement).value);
  }
  
  onPageChange(page: number) {
    this.currentPage.set(page);
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }
  
  onAddToCart(product: Product) {
    this.cartStore.addItem(product);
  }
  
  onAddToWishlist(product: Product) {
    // Wishlist logic
  }
}

Checkout Flow

// features/checkout/checkout.component.ts
@Component({
  selector: 'app-checkout',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    StepperComponent,
    AddressFormComponent,
    PaymentFormComponent,
    OrderSummaryComponent
  ],
  template: `
    <div class="checkout-page">
      <app-stepper [currentStep]="currentStep()">
        <app-step title="Shipping">
          <app-address-form
            [form]="shippingForm"
            [savedAddresses]="savedAddresses()"
            (selectAddress)="onSelectAddress($event)"
          />
        </app-step>
        
        <app-step title="Payment">
          <app-payment-form
            [form]="paymentForm"
            [savedCards]="savedCards()"
          />
        </app-step>
        
        <app-step title="Review">
          <app-order-review
            [items]="cartItems()"
            [shippingAddress]="shippingForm.value"
            [paymentMethod]="paymentForm.value"
          />
        </app-step>
      </app-stepper>
      
      <aside class="order-summary">
        <app-order-summary
          [items]="cartItems()"
          [shipping]="shippingCost()"
          [tax]="tax()"
          [total]="total()"
        />
        
        <button
          class="btn-primary"
          [disabled]="!canProceed()"
          (click)="proceed()"
        >
          {{ buttonText() }}
        </button>
      </aside>
    </div>
  `
})
export class CheckoutComponent {
  private fb = inject(FormBuilder);
  private cartStore = inject(CartStore);
  private orderService = inject(OrderService);
  private router = inject(Router);
  
  currentStep = signal(0);
  processing = signal(false);
  
  cartItems = this.cartStore.items;
  
  shippingForm = this.fb.group({
    firstName: ['', Validators.required],
    lastName: ['', Validators.required],
    address: ['', Validators.required],
    city: ['', Validators.required],
    state: ['', Validators.required],
    zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]],
    phone: ['', Validators.required]
  });
  
  paymentForm = this.fb.group({
    cardNumber: ['', [Validators.required, this.cardValidator]],
    expiry: ['', Validators.required],
    cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]],
    nameOnCard: ['', Validators.required]
  });
  
  buttonText = computed(() => {
    if (this.currentStep() === 2) return 'Place Order';
    return 'Continue';
  });
  
  canProceed = computed(() => {
    switch (this.currentStep()) {
      case 0: return this.shippingForm.valid;
      case 1: return this.paymentForm.valid;
      case 2: return !this.processing();
      default: return false;
    }
  });
  
  proceed() {
    if (this.currentStep() < 2) {
      this.currentStep.update(s => s + 1);
    } else {
      this.placeOrder();
    }
  }
  
  async placeOrder() {
    this.processing.set(true);
    
    try {
      const order = await firstValueFrom(
        this.orderService.createOrder({
          items: this.cartItems(),
          shipping: this.shippingForm.value,
          payment: this.paymentForm.value
        })
      );
      
      this.cartStore.clearCart();
      this.router.navigate(['/orders', order.id, 'confirmation']);
    } catch (error) {
      // Handle error
    } finally {
      this.processing.set(false);
    }
  }
}

Testing Strategy

Unit Tests

// features/cart/cart.store.spec.ts
describe('CartStore', () => {
  let store: InstanceType<typeof CartStore>;
  
  beforeEach(() => {
    TestBed.configureTestingModule({});
    store = TestBed.inject(CartStore);
  });
  
  it('should add item to cart', () => {
    const product = createMockProduct();
    
    store.addItem(product);
    
    expect(store.items()).toHaveLength(1);
    expect(store.items()[0].product).toEqual(product);
  });
  
  it('should increase quantity for existing item', () => {
    const product = createMockProduct();
    
    store.addItem(product);
    store.addItem(product);
    
    expect(store.items()).toHaveLength(1);
    expect(store.items()[0].quantity).toBe(2);
  });
  
  it('should calculate total correctly', () => {
    const product1 = createMockProduct({ price: 100 });
    const product2 = createMockProduct({ price: 50 });
    
    store.addItem(product1, 2);
    store.addItem(product2, 1);
    
    expect(store.subtotal()).toBe(250);
    expect(store.tax()).toBe(25);
    expect(store.total()).toBe(275);
  });
});

E2E Tests

// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/products');
    await page.click('[data-testid="add-to-cart"]');
    await page.click('[data-testid="go-to-checkout"]');
  });
  
  test('should complete checkout successfully', async ({ page }) => {
    // Fill shipping
    await page.fill('[name="firstName"]', 'John');
    await page.fill('[name="lastName"]', 'Doe');
    await page.fill('[name="address"]', '123 Main St');
    await page.fill('[name="city"]', 'New York');
    await page.fill('[name="state"]', 'NY');
    await page.fill('[name="zip"]', '10001');
    await page.click('[data-testid="continue-btn"]');
    
    // Fill payment
    await page.fill('[name="cardNumber"]', '4111111111111111');
    await page.fill('[name="expiry"]', '12/25');
    await page.fill('[name="cvv"]', '123');
    await page.click('[data-testid="continue-btn"]');
    
    // Review and place order
    await page.click('[data-testid="place-order-btn"]');
    
    // Verify confirmation
    await expect(page).toHaveURL(/\/orders\/.*\/confirmation/);
    await expect(page.locator('h1')).toContainText('Order Confirmed');
  });
});

Deployment Checklist

1

Pre-Deployment

  • All tests passing
  • Bundle size optimized (< 200KB initial)
  • Lighthouse score > 90
  • Security audit passed
  • Environment variables configured
2

Build

ng build --configuration production
ng build --configuration production --ssr
3

Deploy

  • Configure CDN for static assets
  • Set up SSL certificates
  • Configure Node.js server for SSR
  • Set up monitoring (Sentry, LogRocket)
4

Post-Deployment

  • Verify all routes work
  • Test checkout flow end-to-end
  • Monitor error rates
  • Set up alerts for critical issues

Congratulations! 🎉

You’ve completed the Angular Master Course! You now have the skills to build enterprise-grade Angular applications.