> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 33. Enterprise Capstone Project

> Build a complete enterprise Angular application from scratch

<Frame>
  <img src="https://mintcdn.com/devweeekends/AEOaWh79Ur7CdHHv/images/courses/angular-crash-course/angular-hero.svg?fit=max&auto=format&n=AEOaWh79Ur7CdHHv&q=85&s=32645ae19fa9bc25d3ec281022aba371" alt="Enterprise Capstone" width="1200" height="400" data-path="images/courses/angular-crash-course/angular-hero.svg" />
</Frame>

## Enterprise Capstone Project

<Info>
  **Estimated Time**: 20+ hours | **Difficulty**: Advanced | **Prerequisites**: All Previous Modules
</Info>

Build a complete enterprise-grade e-commerce platform that implements all concepts covered in this course. This capstone project simulates real-world development scenarios -- not just "make it work" but "make it work the way a production team would ship it," with proper error handling, loading states, accessibility, testing, and deployment configuration.

**How to approach this project**: Do not try to build everything at once. Work in vertical slices: pick one feature (e.g., product listing), build it end-to-end with store, service, component, tests, and error handling. Then move to the next feature. This mirrors how professional teams work and prevents the common trap of having 10 half-finished features and zero shippable ones.

```
┌─────────────────────────────────────────────────────────────────────────┐
│                    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

<CardGroup cols={2}>
  <Card title="Authentication" icon="lock">
    * User registration/login
    * JWT token management
    * Role-based access (User/Admin)
    * Password reset flow
    * Social login (optional)
  </Card>

  <Card title="Product Catalog" icon="store">
    * Product listing with filters
    * Search functionality
    * Category navigation
    * Product details page
    * Image gallery
  </Card>

  <Card title="Shopping Cart" icon="cart-shopping">
    * Add/remove items
    * Quantity updates
    * Price calculations
    * Persistent cart
    * Guest checkout
  </Card>

  <Card title="Checkout & Orders" icon="credit-card">
    * Multi-step checkout
    * Address management
    * Payment integration
    * Order confirmation
    * Order history
  </Card>

  <Card title="User Dashboard" icon="user">
    * Profile management
    * Order tracking
    * Wishlist
    * Reviews & ratings
    * Notifications
  </Card>

  <Card title="Admin Panel" icon="gear">
    * Product CRUD
    * Order management
    * User management
    * Analytics dashboard
    * Inventory control
  </Card>
</CardGroup>

### 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

```bash theme={null}
# 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

```typescript theme={null}
// 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: [
    // eventCoalescing batches multiple change detection triggers from the same
    // event loop tick into a single CD cycle. This matters in forms where a
    // single keystroke can trigger multiple valueChanges emissions.
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(
      routes,
      // Automatically binds route params to component inputs --
      // no more inject(ActivatedRoute) + switchMap boilerplate.
      withComponentInputBinding(),
      // Smooth page transitions using the View Transitions API.
      withViewTransitions()
    ),
    provideHttpClient(
      // withFetch() uses the Fetch API instead of XMLHttpRequest.
      // Required for streaming responses and better SSR compatibility.
      withFetch(),
      // Interceptor order matters: auth adds the token, error handles failures,
      // loading tracks in-flight requests for global spinners.
      withInterceptors([
        authInterceptor,
        errorInterceptor,
        loadingInterceptor
      ])
    ),
    // withEventReplay() captures user interactions during SSR hydration
    // and replays them once the client-side app is ready. Without this,
    // button clicks during hydration are silently lost.
    provideClientHydration(withEventReplay()),
    provideAnimationsAsync()
  ]
};
```

***

## Core Module Implementation

### Authentication Store

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

<Steps>
  <Step title="Pre-Deployment">
    * [ ] All tests passing
    * [ ] Bundle size optimized (\< 200KB initial)
    * [ ] Lighthouse score > 90
    * [ ] Security audit passed
    * [ ] Environment variables configured
  </Step>

  <Step title="Build">
    ```bash theme={null}
    ng build --configuration production
    ng build --configuration production --ssr
    ```
  </Step>

  <Step title="Deploy">
    * [ ] Configure CDN for static assets
    * [ ] Set up SSL certificates
    * [ ] Configure Node.js server for SSR
    * [ ] Set up monitoring (Sentry, LogRocket)
  </Step>

  <Step title="Post-Deployment">
    * [ ] Verify all routes work
    * [ ] Test checkout flow end-to-end
    * [ ] Monitor error rates
    * [ ] Set up alerts for critical issues
  </Step>
</Steps>

***

## Congratulations! 🎉

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

<CardGroup cols={2}>
  <Card title="Review Materials" icon="book" href="/courses/angular-crash-course/00-overview">
    Go back and review any module
  </Card>

  <Card title="Join Community" icon="users" href="https://discord.gg/angular">
    Connect with other Angular developers
  </Card>
</CardGroup>
