The Angular frontend architecture for the classified website applies all the patterns built in Parts 5 and 7: lazy-loaded feature routes, signal-based state, HTTP interceptors, Angular Material theming, and reactive forms. Feature-based organisation keeps the codebase navigable — every piece of code for the “browse” feature lives in features/browse/, making it easy to find, modify, and test without touching other features.
App Architecture and Routing
// ── app.routes.ts — lazy-loaded feature routes ────────────────────────────
export const appRoutes: Routes = [
{ path: '', redirectTo: 'browse', pathMatch: 'full' },
// Public routes
{
path: 'browse',
loadChildren: () => import('./features/browse/browse.routes')
.then(m => m.browseRoutes),
},
{
path: 'listings',
loadChildren: () => import('./features/listings/listings.routes')
.then(m => m.listingsRoutes),
},
{
path: 'sellers',
loadChildren: () => import('./features/sellers/sellers.routes')
.then(m => m.sellersRoutes),
},
// Auth routes (preloaded — users need login fast)
{
path: 'auth',
loadChildren: () => import('./features/auth/auth.routes')
.then(m => m.authRoutes),
},
// Seller-only routes
{
path: 'my-listings',
loadChildren: () => import('./features/seller/seller.routes')
.then(m => m.sellerRoutes),
canActivate: [authGuard],
data: { requiredRole: 'Seller' },
},
// Admin routes
{
path: 'admin',
loadChildren: () => import('./features/admin/admin.routes')
.then(m => m.adminRoutes),
canActivate: [authGuard, roleGuard],
data: { requiredRole: 'Moderator' },
},
{ path: '**', redirectTo: 'browse' },
];
// ── Core signal-based search state ────────────────────────────────────────
@Injectable({ providedIn: 'root' })
export class SearchStateService {
private router = inject(Router);
private route = inject(ActivatedRoute);
// Signals — reactive source of truth for all filters
keyword = signal<string>('');
category = signal<Category | null>(null);
city = signal<string>('');
minPrice = signal<number | null>(null);
maxPrice = signal<number | null>(null);
page = signal<number>(1);
// Derived query params object for API calls
readonly params = computed(() => ({
keyword: this.keyword() || undefined,
category: this.category() ?? undefined,
city: this.city() || undefined,
minPrice: this.minPrice() ?? undefined,
maxPrice: this.maxPrice() ?? undefined,
page: this.page(),
}));
// Sync URL → signals on init
syncFromUrl(params: ParamMap): void {
this.keyword.set(params.get('keyword') ?? '');
this.category.set(params.get('category') as Category ?? null);
this.city.set(params.get('city') ?? '');
this.minPrice.set(params.get('minPrice') ? +params.get('minPrice')! : null);
this.maxPrice.set(params.get('maxPrice') ? +params.get('maxPrice')! : null);
this.page.set(params.get('page') ? +params.get('page')! : 1);
}
// Sync signals → URL
updateUrl(partial: Partial<SearchParams>): void {
this.router.navigate(['/browse'], {
queryParams: {
keyword: partial.keyword ?? this.keyword() || null,
category: partial.category ?? this.category() ?? null,
city: partial.city ?? this.city() || null,
minPrice: partial.minPrice ?? this.minPrice() ?? null,
maxPrice: partial.maxPrice ?? this.maxPrice() ?? null,
page: partial.page ?? 1, // reset to page 1 on filter change
},
});
}
clearAll(): void {
this.router.navigate(['/browse']);
}
}
SearchStateService is the single source of truth for all search filter state. Components read from and write to this service rather than managing their own filter state. The service synchronises bidirectionally with the URL — URL changes update the signals, signal changes update the URL. This means the browser back button restores previous search state, and sharing a URL restores the exact same search. This is the correct architecture for any multi-filter search page.data: { requiredRole: 'Seller' } on routes combined with a roleGuard that reads the required role from route data. This keeps role requirements co-located with route definitions (easy to audit — one file shows all protected routes) rather than hardcoded in individual guards. The roleGuard reads route.data['requiredRole'] and checks it against the current user’s claims, redirecting to the seller registration page if the role is missing.PreloadAllModules strategy (or a custom preloading strategy) to background-preload other feature chunks after the initial route loads — eliminating the delay for subsequent navigations while keeping the initial bundle small.Common Mistakes
Mistake 1 — Global mutable state in services (search filters reset on navigation)
❌ Wrong — component-local signals for filters; navigating away then back resets all filters; poor UX.
✅ Correct — SearchStateService (provided in root) persists filter state across navigation; URL persistence for share/bookmark.
Mistake 2 — Eager-loading all feature modules (large initial bundle)
❌ Wrong — all routes in the main bundle; initial bundle 2MB+; slow first load especially on mobile.
✅ Correct — lazy-loaded feature routes; initial bundle ~200KB; each feature loaded on demand.