Mastering Modular Architecture in Angular
Sehban Alam
Software Engineer (Creating Scalable, Secure, Cloud-Powered Applications that Redefine B2B/B2C User Experiences) | Angular | Firebase | Cloudflare | Material Design | Serverless | MySQL | GCP | AWS
As Angular applications grow in size and complexity, organizing the code efficiently becomes a critical challenge. Modular architecture, which breaks an application into smaller, self-contained modules, is a powerful solution. This post will explore how to implement a modular structure in Angular, its benefits, real-world use cases, and how to structure the directories for optimal organization.
What is Modular Architecture?
Modular architecture in Angular refers to dividing an application into smaller, reusable, and maintainable chunks called “modules.” Each module encapsulates a set of related components, services, directives, pipes, and other dependencies. Angular itself revolves around the concept of NgModules, making modular design a natural fit for any Angular project.
Why is Modular Architecture Important?
Modular Structure Overview
The basic idea is to split your app into feature modules, each containing related components, services, and other elements. Angular already follows a modular approach by dividing the core features like forms, routing, and HTTP into separate modules. You can do the same for your application features.
Advantages of Modular Architecture
Real-World Example: Building an E-commerce Application with Modular Architecture
Let’s dive deeper into an example of building a modular e-commerce application. The application will have the following features:
Each feature will be encapsulated within its own module, and we will lazy-load these modules when necessary.
Step 1: Organizing the Application into Feature Modules
Here’s the folder structure for the e-commerce app:
/src
|-- /app
|-- /core # Core services and singleton utilities like auth, logging
|-- /shared # Shared components, pipes, directives
|-- /user # User module (user registration, login, profile)
|-- /product # Product module (product listing, details, reviews)
|-- /cart # Cart module (adding to cart, reviewing, checkout)
|-- /order # Order module (order history, tracking)
|-- /app.module.ts # Root module
|-- /app-routing.module.ts # Root routing module
Step 2: Creating the Feature Modules
User Module (user.module.ts)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserProfileComponent } from './components/user-profile.component';
import { UserLoginComponent } from './components/user-login.component';
import { UserService } from './services/user.service';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: 'login', component: UserLoginComponent },
{ path: 'profile', component: UserProfileComponent }
];
@NgModule({
declarations: [UserProfileComponent, UserLoginComponent],
imports: [
CommonModule,
FormsModule,
RouterModule.forChild(routes) // Defining routes within the module
],
providers: [UserService],
exports: [UserProfileComponent, UserLoginComponent]
})
export class UserModule { }
user-profile.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit {
user: any;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.user = this.userService.getUserProfile();
}
}
Product Module (product.module.ts)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductListComponent } from './components/product-list.component';
import { ProductDetailsComponent } from './components/product-details.component';
import { ProductService } from './services/product.service';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductDetailsComponent }
];
@NgModule({
declarations: [ProductListComponent, ProductDetailsComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
providers: [ProductService],
exports: [ProductListComponent, ProductDetailsComponent]
})
export class ProductModule { }
领英推荐
product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../services/product.service';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
products: any[] = [];
constructor(private productService: ProductService) {}
ngOnInit(): void {
this.products = this.productService.getProducts();
}
}
Cart Module (cart.module.ts)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CartComponent } from './components/cart.component';
import { CheckoutComponent } from './components/checkout.component';
import { CartService } from './services/cart.service';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: 'cart', component: CartComponent },
{ path: 'checkout', component: CheckoutComponent }
];
@NgModule({
declarations: [CartComponent, CheckoutComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
providers: [CartService],
exports: [CartComponent, CheckoutComponent]
})
export class CartModule { }
cart.component.ts
import { Component, OnInit } from '@angular/core';
import { CartService } from '../services/cart.service';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.css']
})
export class CartComponent implements OnInit {
cartItems: any[] = [];
constructor(private cartService: CartService) {}
ngOnInit(): void {
this.cartItems = this.cartService.getCartItems();
}
}
Order Module (order.module.ts)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OrderHistoryComponent } from './components/order-history.component';
import { OrderService } from './services/order.service';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: 'orders', component: OrderHistoryComponent }
];
@NgModule({
declarations: [OrderHistoryComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
providers: [OrderService],
exports: [OrderHistoryComponent]
})
export class OrderModule { }
order-history.component.ts
import { Component, OnInit } from '@angular/core';
import { OrderService } from '../services/order.service';
@Component({
selector: 'app-order-history',
templateUrl: './order-history.component.html',
styleUrls: ['./order-history.component.css']
})
export class OrderHistoryComponent implements OnInit {
orders: any[] = [];
constructor(private orderService: OrderService) {}
ngOnInit(): void {
this.orders = this.orderService.getOrderHistory();
}
}
Step 3: Lazy Loading the Modules
To optimize performance, we will lazy-load these feature modules when a user navigates to the respective route. This reduces the initial bundle size and speeds up the application load time.
Here’s how the AppRoutingModule will look:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: 'user', loadChildren: () => import('./user/user.module').then(m => m.UserModule) },
{ path: 'products', loadChildren: () => import('./product/product.module').then(m => m.ProductModule) },
{ path: 'cart', loadChildren: () => import('./cart/cart.module').then(m => m.CartModule) },
{ path: 'orders', loadChildren: () => import('./order/order.module').then(m => m.OrderModule) },
{ path: '', redirectTo: '/products', pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
In this setup:
Advantages of This Setup
Conclusion
Modular architecture in Angular offers a robust, scalable, and maintainable way to build large applications. By organizing features into modules, you separate concerns, making it easier to develop, maintain, and extend the application over time. Each module can encapsulate components, services, directives, and more, promoting reusability and independence.
With this modular structure, you also get the benefit of lazy loading, which optimizes performance by loading feature modules only when they are required. This is especially useful in large applications like e-commerce platforms, where some features may not be needed until specific user interactions occur.
This approach also makes testing simpler and faster, as you can test each module independently. It enables parallel development, where multiple teams or developers can work on different modules without interfering with each other, leading to faster development cycles.
In essence, the modular architecture gives your Angular application the flexibility and scalability it needs to grow while keeping the codebase manageable. This structure is not just about improving performance but also about fostering good coding practices and creating a maintainable and future-proof application.
By following these principles and using real-world examples, your Angular projects will not only be easier to develop but also more adaptable to future changes, ensuring long-term success in large-scale applications.