Files
gh-ehssanatassi-angular-mar…/skills/router-first-methodology/SKILL.md
2025-11-29 18:24:55 +08:00

10 KiB

Router-First Methodology

Author: Doguhan Uluca
Source: Angular for Enterprise Applications, 3rd Edition
Context: Enterprise Angular architecture for teams of 5-100+ developers


Core Concept

Router-First Architecture is a methodology that enforces designing your application's routing structure BEFORE implementing components. This approach ensures high-level thinking, team consensus, and scalable architecture from day one.


Why Router-First?

Traditional development often starts with components, leading to:

  • Unclear application structure
  • Tight coupling between features
  • Difficult to refactor later
  • Hard to parallelize team work
  • Performance issues at scale

Router-First solves this by:

  • Forcing architectural decisions early
  • Creating clear feature boundaries
  • Enabling lazy loading from the start
  • Facilitating team collaboration
  • Making the app structure visible in code

The 7 Steps

Step 1: Develop a Roadmap and Scope

Goal: Define what features your application needs

Process:

  1. List all user-facing features
  2. Identify MVP vs. future features
  3. Group related functionality
  4. Define user roles and permissions

Example:

E-commerce App Roadmap:

Phase 1 (MVP):
- Product browsing
- Shopping cart
- Checkout
- User authentication

Phase 2:
- Order history
- Product reviews
- Wishlist
- Admin panel

Phase 3:
- Analytics dashboard
- Inventory management
- Customer support

Output: Feature list with priorities


Step 2: Design with Lazy Loading in Mind

Goal: Plan bundle structure for optimal performance

Process:

  1. Each major feature = separate lazy-loaded module
  2. Identify shared dependencies
  3. Plan loading strategies
  4. Set bundle size budgets

Example:

// Bundle planning
Initial Load (Critical Path):
- Authentication (50 KB)
- Layout shell (30 KB)
- Core services (40 KB)
Total: 120 KB 

Lazy Loaded Features:
- Dashboard (60 KB)
- Products (80 KB) 
- Orders (45 KB)
- Admin (120 KB)

Strategy:
- Preload Dashboard after login
- Lazy load others on-demand
- Code split large features

Anti-pattern:

// ❌ BAD: Everything imported at root
import { DashboardModule } from './dashboard';
import { ProductsModule } from './products';
import { OrdersModule } from './orders';

Best Practice:

// ✅ GOOD: Lazy loaded via routes
{
  path: 'dashboard',
  loadChildren: () => import('./dashboard/dashboard.routes')
}

Step 3: Implement Walking-Skeleton Navigation

Goal: Create navigable shell with placeholder content

Process:

  1. Define all routes in app.routes.ts
  2. Create shell components (empty templates)
  3. Verify navigation works
  4. Add breadcrumbs and titles

Example:

// app.routes.ts - Walking skeleton
export const routes: Routes = [
  {
    path: '',
    redirectTo: '/dashboard',
    pathMatch: 'full'
  },
  {
    path: 'dashboard',
    loadComponent: () => import('./features/dashboard/dashboard.component')
      .then(m => m.DashboardComponent),
    data: { breadcrumb: 'Dashboard' }
  },
  {
    path: 'products',
    loadComponent: () => import('./features/products/products.component')
      .then(m => m.ProductsComponent),
    data: { breadcrumb: 'Products' }
  },
  {
    path: 'orders',
    loadComponent: () => import('./features/orders/orders.component')
      .then(m => m.OrdersComponent),
    data: { breadcrumb: 'Orders' }
  }
];
// dashboard.component.ts - Shell component
@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: `
    <h1>Dashboard</h1>
    <p>Coming soon...</p>
  `
})
export class DashboardComponent {}

Benefit: Team can navigate the app before any features are implemented


Step 4: Achieve Stateless, Data-Driven Design

Goal: Components receive data, don't manage global state

Process:

  1. Services handle state and HTTP
  2. Components receive data via inputs/signals
  3. Components emit events, not side effects
  4. Use observables for async data

Example:

// ❌ BAD: Component manages state
@Component({...})
export class ProductListComponent {
  products: Product[] = [];
  
  constructor(private http: HttpClient) {
    this.http.get('/api/products').subscribe(data => {
      this.products = data;
    });
  }
}
// ✅ GOOD: Service manages state
@Injectable({ providedIn: 'root' })
export class ProductService {
  private products$ = new BehaviorSubject<Product[]>([]);
  
  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>('/api/products').pipe(
      tap(products => this.products$.next(products))
    );
  }
}

@Component({...})
export class ProductListComponent {
  products$ = inject(ProductService).getProducts();
}

Step 5: Enforce Decoupled Component Architecture

Goal: Separate smart (container) and dumb (presentational) components

Smart Components:

  • Manage data fetching
  • Handle business logic
  • Communicate with services
  • Located in feature folders

Dumb Components:

  • Receive data via @Input
  • Emit events via @Output
  • No business logic
  • Located in shared folder

Example:

// Smart component (container)
@Component({
  selector: 'app-product-list',
  template: `
    @for (product of products(); track product.id) {
      <app-product-card 
        [product]="product"
        (addToCart)="handleAddToCart($event)"
      />
    }
  `
})
export class ProductListComponent {
  private productService = inject(ProductService);
  products = toSignal(this.productService.getProducts());
  
  handleAddToCart(productId: string) {
    this.cartService.addItem(productId);
  }
}

// Dumb component (presentational)
@Component({
  selector: 'app-product-card',
  template: `
    <div class="card">
      <h3>{{ product.name }}</h3>
      <p>{{ product.price | currency }}</p>
      <button (click)="addToCart.emit(product.id)">
        Add to Cart
      </button>
    </div>
  `
})
export class ProductCardComponent {
  @Input({ required: true }) product!: Product;
  @Output() addToCart = new EventEmitter<string>();
}

Step 6: Differentiate User Controls vs Components

Goal: Clear separation between reusable UI and feature-specific components

User Controls (Shared):

  • Generic UI elements
  • No business logic
  • Highly reusable
  • Location: shared/components/

Feature Components:

  • Feature-specific logic
  • Use shared controls
  • Business logic included
  • Location: features/<feature>/components/

Example Structure:

shared/components/          # User Controls
├── button/
├── input/
├── card/
├── modal/
└── data-table/

features/products/          # Feature Components
├── product-list/
├── product-detail/
├── product-form/
└── product-search/

Step 7: Maximize Code Reuse

Goal: DRY principle with TypeScript and ES features

Techniques:

  1. Shared Utilities
// shared/utils/date.utils.ts
export function formatDate(date: Date): string {
  return date.toLocaleDateString('en-US');
}
  1. Shared Interfaces
// core/models/api-response.interface.ts
export interface ApiResponse<T> {
  data: T;
  message: string;
  status: number;
}
  1. Base Classes (use sparingly)
// core/base/base-component.ts
export abstract class BaseComponent implements OnDestroy {
  protected destroy$ = new Subject<void>();
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
  1. Mixins
// shared/mixins/timestamp.mixin.ts
export function WithTimestamp<T extends Constructor>(Base: T) {
  return class extends Base {
    createdAt = new Date();
    updatedAt = new Date();
  };
}

Real-World Application

Case Study: E-commerce Platform

Team: 15 developers
Timeline: 6 months
Features: 12 major features

Router-First Implementation:

  1. Week 1: Route planning

    • Defined all 12 features as routes
    • Created walking skeleton
    • Team reviewed and agreed on structure
  2. Week 2-3: Core setup

    • Implemented auth guards
    • Set up core services
    • Created shared components
  3. Week 4-24: Parallel development

    • 3 teams worked on different features simultaneously
    • No merge conflicts (clear boundaries)
    • Easy to track progress (routes visible)
  4. Result:

    • On-time delivery
    • 185 KB initial bundle
    • 45 KB average feature bundle
    • Easy onboarding for new devs

Common Mistakes

1. Starting with Components

// ❌ WRONG ORDER
1. Build dashboard component
2. Build product list component  
3. Figure out routing later

// ✅ CORRECT ORDER
1. Design routes
2. Create shell components
3. Implement features

2. Tight Coupling

// ❌ BAD: Direct component dependencies
export class DashboardComponent {
  constructor(private productList: ProductListComponent) {}
}

// ✅ GOOD: Service-based communication
export class DashboardComponent {
  constructor(private productService: ProductService) {}
}

3. Ignoring Lazy Loading

// ❌ BAD: Eager loading everything
imports: [
  DashboardModule,
  ProductsModule,
  OrdersModule
]

// ✅ GOOD: Lazy load features
{
  path: 'dashboard',
  loadChildren: () => import('./dashboard/dashboard.routes')
}

Checklist

Before claiming Router-First compliance:

  • Routes defined before component implementation
  • All features lazy loaded (except critical path)
  • Walking skeleton navigation works
  • Smart/Dumb component separation
  • Services manage state, not components
  • Shared components in shared folder
  • Feature components in feature folders
  • Clear team agreement on structure
  • Bundle size budgets defined
  • Documentation of routing decisions

Summary

Router-First Architecture is about planning before building. By designing routes first, you create a scalable, maintainable, and performant Angular application that grows with your team.

Key Takeaway: If you can see your entire application structure by looking at app.routes.ts, you're doing it right.