Enterprise Capstone Project
Estimated Time: 20+ hours | Difficulty: Advanced | Prerequisites: All Previous Modules
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ 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
Copy
# 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
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