In software development, grouping multiple specific functionalities or features into different modules is the key to building an application that is scalable, maintainable, and manageable. When it comes to Angular, NgModule is a great way to organize and refactor an application. This article will show you how to define and understand the different types of Angular NgModules with practical examples.
How to define an Angular Module?
NgModule is a way to group code blocks with related domains, workflow, or capabilities. It describes how the Angular applications organize and fit together. An NgModule is a class marked by the @NgModule decorator. @NgModule decorator takes a metadata object whose properties are used to declare what we need in the templates of the components.
The most important @NgModule metadata properties are used to define a NgModule as follows.
@NgModule({
imports: [],
declarations: [],
providers: [],
exports: [],
bootstrap: []
})
- Declarations: The set of components, directives, and pipes that belong to this module.
- Imports: The set of NgModules whose exported components, directives, or pipes are used in the component templates of this module.
- Providers: Contains the list of injectable service classes that will be available in the injector of this module.
- Exports: A list of declarations and NgModules that an importing module can use.
- Bootstrap: The set of components that are bootstrapped at the same time as this module. Angular can launch with multiple bootstrap components whose multiple component trees are on a host web page. But usually, most applications have only one component tree and bootstrap a single root component.
Types of Angular Modules and How to define them?
Root NgModule
Each application only has one root NgModule to bootstrap the application on launch. The default name for this root module is often AppModule, but it is possible to change the name.
This is an example of an Application Root module.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}
The root NgModule takes metadata properties that describe how Angular compiles and launches an application.
- Declarations: The root AppModule has the root component called AppComponent, which is in both the declarations and the bootstrap arrays used to bootstrap the application. The bootstrapping process will create the AppComponent and insert it into the DOM.
- Imports: BrowserModule is imported to provide services essential to launch and run a browser application, such as DOM rendering, sanitization, and location. The BrowserModule itself re-exports CommonModule, so we don’t need to import CommonModule in the AppModule.
- Providers: All services provided in this root NgModule using the providers’ array will be available to the entire application and loaded only once.
The last thing to keep in mind is that the AppModule is the root of your application and should only be used for bootstrapping. We shouldn't utilize it for importing to another NgModule or for any other purpose. That’s why we don’t export any declarations in the AppModule.
Domain NgModule
A module that offers a user experience tailored to a feature or application domain is referred to as a domain NgModule. It may be called a feature module. A domain module can be subdivided into smaller domain modules based on its complexity.
For instance, the ProductDetail domain module contains smaller modules, such as
ProductIntroduction, ProductCard, ProductDescription, ProductPromotion, ProductComments.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
//
import { ProductDetailComponent } from '@app/modules/products/components';
import { ProductIntroductionModule } from '@app/modules/products/introduction';
import { ProductCardModule } from '@app/modules/products/card-info';
import { ProductDescriptionModule } from '@app/modules/products/description';
import { ProductPromotionModule } from '@app/modules/products/promotion';
import { ProductCommentsModule } from '@app/modules/products/comments';
import { ProductsService } from '@app/modules/products/services';
@NgModule({
declarations: [ProductDetailComponent],
imports: [
CommonModule,
ProductIntroductionModule,
ProductCardModule,
ProductDescriptionModule,
ProductPromotionModule,
ProductCommentsModule
],
providers: [ProductsService]
})
export class ProductDetailModule {}
ProductDetailModule consists of all the declarations and imported domain modules for a specific business. ProductDetailModule is imported exactly once into root NgModule to handle the product detail page. While another ProductCardModule can be imported into other domain NgModules, like CartModule.
Routing NgModule
A routing NgModule is a module that provides the routing configuration for a domain NgModule, including AppModule.
Naming for a routing NgModule should follow its domain NgModule name and include the suffix Routing. For example, ProductsModule in products.module.ts contains a routing NgModule called ProductsRoutingModule in products-routing.module.ts.
When importing RouterModule in NgModules, the routes are registered using the forRoot() and forChild() static methods.
- RouterModule.forRoot(routes) is imported in AppRoutingModule to specify that module is the root routing module.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './page-not-found.component';
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./modules/products/products.module').then(m => m.ProductsModule),
},
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
- RouterModule.forChild(routes) is imported into another routing NgModules.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProductsComponent } from '@app/products/components';
const routes: Routes = [
{
path: '',
component: ProductsComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProductsRoutingModule { }
When its router configuration is too long or complicated, we only define a routing NgModule separately with a domain NgModule. It makes our code cleaner.
The routing NgModule doesn't declare anything, including components, pipes, and directives. This is because these declarations should be in the domain NgModule instead.
We can add guard and resolver services to the NgModule's providers as well as always export RouterModule, so all the template’s components in the domain NgModule can use RouterModule service and directives.
Routed NgModule
Routed NgModules are defined as lazy-loaded NgModule. Using lazy-loaded NgModules helps keep initial bundle sizes smaller and decreases the app’s load times. Besides, we don’t import a lazy-loaded routed NgModule into another NgModule since it would be eagerly loaded.
For example, we have an application with the Products page, whose URL is http://localhost:4200/products.
To define ProductsModule as lazy-loaded NgModule, in ProductsModule routes configuration, we add a route for the ProductsComponent as the destination of the navigation route each time we navigate to http://localhost:4200/products.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { ProductsComponent } from '@app/products/components';
const routes: Routes = [
{
path: '',
component: ProductsComponent
}
];
@NgModule({
declarations: [ProductsComponent],
imports: [
CommonModule,
RouterModule.forChild(routes)
]
})
export class ProductsModule {}
To lazy load ProductsModule, we use loadChildren in a routing NgModule’s routes configuration.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
//
import { PageNotFoundComponent } from './page-not-found.component';
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./modules/products/products.module').then(m => m.ProductsModule),
},
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
We don’t import a lazy-loaded routed NgModule into another NgModule because it would be eagerly loaded, so this defeats the purpose of lazy loading. That’s why it doesn’t export anything. Rarely does it declare providers.
Service NgModule
A Service NgModule is a module that provides a utility service that is useful in a variety of situations, such as:
- Data access: We have a lot of built-in services NgModule in Angular, including HttpClientModule, CommonModule, RouterModule, and so on.
- State Management: There are many state management libraries, including NgRx, NGXS, etc.
- Messaging
- Handle logics for Domain NgModules
The service NgModule should provide services and have no declarations. The root AppModule only imports it.
Widget NgModule
The widget NgModules are modules that provide components, directives, or pipes for other modules that need them in their templates. For example, most third-party UI component libraries are provided as widget NgModules, such as DevExtreme Framework UI and Angular Material, to name a few.
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {
DxTextBoxModule
} from 'devextreme-angular';
const BASE_MODULES = [
CommonModule
];
const DEVEXTREME_MODULES = [
DxTextBoxModule
];
@NgModule({
declarations: [],
imports: [
...BASE_MODULES,
...DEVEXTREME_MODULES,
],
providers: [],
exports: [
...BASE_MODULES,
...DEVEXTREME_MODULES,
]
})
export class SharedModule {
}
A widget NgModule should comprise declarations, and most of them are exported. It would rarely have providers.
Shared NgModule
SharedModule is usually used to put all reusable components, directives, and pipes that other NgModules need.
Consider the following example:
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {
DxButtonModule, DxCheckBoxModule,
DxTextAreaModule, DxPopupModule,
DxNumberBoxModule, DxTextBoxModule
} from 'devextreme-angular';
import { SidebarMenuComponent, PopupComponent } from './components';
import {
AutoFocusInputDirective,
} from './directives';
const DEVEXTREME_MODULES = [
DxButtonModule, DxCheckBoxModule,
DxTextAreaModule, DxPopupModule,
DxNumberBoxModule, DxTextBoxModule
];
const BASE_MODULES = [
CommonModule, RouterModule,
];
const COMPONENTS = [ SidebarMenuComponent, PopupComponent ];
const DIRECTIVES = [ AutoFocusInputDirective ];
const PIPES = [ TimeAgoPipe ];
@NgModule({
declarations: [
...COMPONENTS,
...DIRECTIVES,
...PIPES
],
imports: [
...BASE_MODULES,
...DEVEXTREME_MODULES,
],
providers: [],
exports: [
...BASE_MODULES,
...DEVEXTREME_MODULES,
...COMPONENTS,
...DIRECTIVES,
...PIPES
]
})
export class SharedModule {}
Angular’s built-in modules and DevExtreme’s widget modules are defined into BASE_MODULES and DEVEXTREME_MODULES, respectively. These constants can be imported, exported as well as used by components, pipes, and directives in SharedModule or other NgModules. Other modules that import this SharedModule can access declarations and providers by re-exporting these modules.
If these modules aren't used in SharedModule, we can only export them without importing them. Except for DxPopupModule, the other widget DevExtreme Modules may not need for SharedModule because the PopupComponent uses only DxPopupModule.
Table summaries for each type
Let’s summarize the types of NgModules in the following table.
NGMODULE | DECLARATIONS | PROVIDERS | EXPORTS | IMPORTED BY |
Root | Yes | Yes | No | None |
Domain | Yes | Rare | Top component | Another domain, AppModule |
Routing | No | Yes (Guards, Resolvers) | RouterModule | Another domain (for routing) |
Routed | Yes | Rare | No | None |
Service | No | Yes | No | AppModule |
Widget | Yes | Rare | Yes | Another domain |
Shared | Yes | No | Yes | Another domain |
Final thoughts,
We walked through the various types of Angular NgModules with practical examples. We hope this sharing can help you gain insight into organizing and structuring the Angular application with separate functionality and business logic using NgModule. A thorough understanding will not only allow you to build a robust application in the future but also provide clear guidelines to your developer team.
Happy coding!
Reference resources
- Guidelines for creating NgModules, angular.io.
- NgModule API, angular.io.
- Achilles Moraites, Types of Modules in Angular, dev.to, 2020.
- Giancarlo Buomprisco, Understanding Angular Modules, giancarlobuomprisco.com, 2019.