| name | router-first-methodology |
| description | Doguhan Uluca's Router-First Architecture - The 7 steps for designing scalable Angular applications by defining routes before components |
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:
- List all user-facing features
- Identify MVP vs. future features
- Group related functionality
- 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:
- Each major feature = separate lazy-loaded module
- Identify shared dependencies
- Plan loading strategies
- Set bundle size budgets
Example:
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:
import { DashboardModule } from './dashboard';
import { ProductsModule } from './products';
import { OrdersModule } from './orders';
Best Practice:
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.routes')
}
Step 3: Implement Walking-Skeleton Navigation
Goal: Create navigable shell with placeholder content
Process:
- Define all routes in app.routes.ts
- Create shell components (empty templates)
- Verify navigation works
- Add breadcrumbs and titles
Example:
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' }
}
];
@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:
- Services handle state and HTTP
- Components receive data via inputs/signals
- Components emit events, not side effects
- Use observables for async data
Example:
@Component({...})
export class ProductListComponent {
products: Product[] = [];
constructor(private http: HttpClient) {
this.http.get('/api/products').subscribe(data => {
this.products = data;
});
}
}
@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:
@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);
}
}
@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:
- Shared Utilities
export function formatDate(date: Date): string {
return date.toLocaleDateString('en-US');
}
- Shared Interfaces
export interface ApiResponse<T> {
data: T;
message: string;
status: number;
}
- Base Classes (use sparingly)
export abstract class BaseComponent implements OnDestroy {
protected destroy$ = new Subject<void>();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
- Mixins
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:
-
Week 1: Route planning
- Defined all 12 features as routes
- Created walking skeleton
- Team reviewed and agreed on structure
-
Week 2-3: Core setup
- Implemented auth guards
- Set up core services
- Created shared components
-
Week 4-24: Parallel development
- 3 teams worked on different features simultaneously
- No merge conflicts (clear boundaries)
- Easy to track progress (routes visible)
-
Result:
- On-time delivery
- 185 KB initial bundle
- 45 KB average feature bundle
- Easy onboarding for new devs
Common Mistakes
1. Starting with Components
1. Build dashboard component
2. Build product list component
3. Figure out routing later
1. Design routes
2. Create shell components
3. Implement features
2. Tight Coupling
export class DashboardComponent {
constructor(private productList: ProductListComponent) {}
}
export class DashboardComponent {
constructor(private productService: ProductService) {}
}
3. Ignoring Lazy Loading
imports: [
DashboardModule,
ProductsModule,
OrdersModule
]
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.routes')
}
Checklist
Before claiming Router-First compliance:
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.