NgModules
NgModules are Angular's original mechanism for organizing an application into cohesive blocks of functionality. An NgModule is a class decorated with @NgModule that declares a compilation context for components, directives, and pipes — and configures the dependency injection system.
While Angular 14+ introduced standalone components that can work without NgModules, NgModules remain common in existing codebases and are important to understand.
What Is an NgModule?
An NgModule is a container that groups:
- Declarations — components, directives, and pipes that belong to this module
- Imports — other modules whose exported declarations this module needs
- Exports — what this module makes available to other modules
- Providers — services registered with this module's injector
- Bootstrap — the root component (root module only)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './components/header.component';
import { FooterComponent } from './components/footer.component';
@NgModule({
declarations: [
AppComponent, // root component
HeaderComponent, // components declared in this module
FooterComponent,
],
imports: [
BrowserModule, // core browser functionality
CommonModule, // NgIf, NgFor, pipes, etc.
ReactiveFormsModule, // reactive forms support
HttpClientModule, // HTTP client
AppRoutingModule, // routing configuration
],
providers: [
// services registered here are available app-wide
// (unless already using providedIn: 'root')
],
bootstrap: [AppComponent], // root module only
})
export class AppModule {}The Root Module (AppModule)
Every module-based Angular app has exactly one root module, conventionally called AppModule. It is the entry point that Angular uses to bootstrap the application.
The root module is bootstrapped in main.ts:
// main.ts (module-based app)
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));bootstrapApplication() with standalone components instead of bootstrapModule(AppModule). Both approaches are valid and widely used.Feature Modules
As apps grow, you organize functionality into feature modules. Each feature module encapsulates everything related to one area of the app:
// user/user.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { UserRoutingModule } from './user-routing.module';
import { UserListComponent } from './user-list/user-list.component';
import { UserDetailComponent } from './user-detail/user-detail.component';
import { UserFormComponent } from './user-form/user-form.component';
import { UserService } from './user.service';
@NgModule({
declarations: [
UserListComponent,
UserDetailComponent,
UserFormComponent,
],
imports: [
CommonModule,
ReactiveFormsModule,
UserRoutingModule,
],
providers: [UserService],
exports: [
// Only export if other modules need these components
UserListComponent,
],
})
export class UserModule {}Shared Modules
A shared module bundles commonly needed declarations and re-exports them so feature modules import one thing instead of many:
// shared/shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
// Custom shared components/pipes/directives
import { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component';
import { TruncatePipe } from './pipes/truncate.pipe';
import { HighlightDirective } from './directives/highlight.directive';
import { ButtonComponent } from './ui/button/button.component';
@NgModule({
declarations: [
LoadingSpinnerComponent,
TruncatePipe,
HighlightDirective,
ButtonComponent,
],
imports: [
CommonModule,
RouterModule,
],
exports: [
// Re-export Angular modules so feature modules get them too
CommonModule,
ReactiveFormsModule,
FormsModule,
RouterModule,
// Export custom declarations
LoadingSpinnerComponent,
TruncatePipe,
HighlightDirective,
ButtonComponent,
],
})
export class SharedModule {}providedIn: 'root'. Shared modules that provide services create unexpected singleton issues.Lazy Loading with NgModules
Lazy loading splits your app into smaller JavaScript bundles that load only when a route is visited. This dramatically improves initial load time.
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('./features/home/home.module').then(m => m.HomeModule),
},
{
path: 'products',
loadChildren: () =>
import('./features/products/products.module').then(m => m.ProductsModule),
},
{
path: 'admin',
loadChildren: () =>
import('./features/admin/admin.module').then(m => m.AdminModule),
// Route-level providers
// Services in AdminModule are only instantiated when this route loads
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}// features/products/products.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '../../shared/shared.module';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
const routes: Routes = [
{ path: '', component: ProductListComponent },
{ path: ':id', component: ProductDetailComponent },
];
@NgModule({
declarations: [ProductListComponent, ProductDetailComponent],
imports: [
SharedModule,
RouterModule.forChild(routes), // forChild for feature modules
],
})
export class ProductsModule {}RouterModule.forRoot(routes) only in the root module. All feature modules use RouterModule.forChild(routes).Core Module Pattern
A core module is a convention for services and components that should exist only once in the app (singleton concerns):
// core/core.module.ts
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';
import { NavbarComponent } from './navbar/navbar.component';
import { NotFoundComponent } from './not-found/not-found.component';
@NgModule({
declarations: [NavbarComponent, NotFoundComponent],
imports: [CommonModule],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
exports: [NavbarComponent],
})
export class CoreModule {
// Guard: throw if CoreModule is imported more than once
constructor(@Optional() @SkipSelf() parentModule?: CoreModule) {
if (parentModule) {
throw new Error('CoreModule is already loaded. Import it only in AppModule.');
}
}
}NgModule declarations vs imports vs exports
Property | Contains | Makes available to |
|---|---|---|
declarations | Components, directives, pipes defined in this module | Templates within this module only |
imports | Other NgModules | Templates in this module (via imports exported declarations) |
exports | Components, directives, pipes, or whole modules | Other modules that import this module |
providers | Services, InjectionToken values | The entire app (for root) or module tree (for feature) |
bootstrap | Root component (AppModule only) | N/A — this is the entry point |
NgModules vs Standalone Components
Angular 14+ standalone components replace NgModules for most purposes. Here is when to choose each:
Aspect | NgModules | Standalone Components |
|---|---|---|
Introduced | Angular 2 | Angular 14 (stable in 15) |
Boilerplate | High — module file per feature | Low — just component file |
Lazy loading | loadChildren: module | loadComponent: component |
Tree-shaking | Module-level | Component-level (better) |
Existing codebases | Common | Migrating to standalone |
Angular recommendation | Legacy / large teams | New projects (default in 17+) |
// Equivalent lazy loading with standalone component
const routes: Routes = [
{
path: 'products',
loadComponent: () =>
import('./features/products/product-list.component')
.then(m => m.ProductListComponent),
},
];Migrating to Standalone
Angular CLI provides an automatic migration to convert NgModule-based apps to standalone:
# Migrate components one by one ng generate @angular/core:standalone # Choose migration mode: # 1. Convert all components/directives/pipes to standalone # 2. Remove unnecessary NgModules # 3. Bootstrap with standalone API
Common NgModule Errors
Component X is not a known element — the component's module is not imported
Can't bind to 'formGroup' — ReactiveFormsModule is not imported
Unexpected component X — component is declared in two modules; move to SharedModule and export
NullInjectorError — service is in a lazy module but being injected in the root; use providedIn: 'root'
Type ComponentX is not part of any NgModule — component declared but module not imported into AppModule
Summary
NgModules are Angular's original code organization system:
- declarations: own components, directives, and pipes
- imports: modules providing needed functionality
- exports: what other modules can use from this module
- providers: services scoped to this module
- Lazy loading: use
loadChildrento split bundles by feature - Core module: single-instance services and app-wide components
- Shared module: reusable UI components and pipes
While standalone components are Angular's future, NgModules remain essential knowledge for the vast majority of production Angular applications in use today.