Introduction to NgModules
Before standalone components arrived, every Angular application was organized around NgModules. An NgModule is a class decorated with @NgModule that groups related components, directives, pipes, and services into a cohesive, reusable unit. Even though modern Angular (17/18/19) treats standalone components as the default, NgModules still power countless existing codebases and many third-party libraries, so understanding how they work is essential when you read older code, migrate apps, or integrate dependencies that haven’t gone standalone yet.
What an NgModule actually does
An NgModule has two jobs. First, it declares which components, directives, and pipes belong to it so the template compiler knows they exist. Second, it controls dependency visibility — what this module needs from the outside (imports), what it makes available to other modules (exports), and which services it registers (providers). The crucial mental model is the compilation context: a declarable (component/directive/pipe) can only use another declarable if they are declared in the same module, or if the second one is exported by a module that the first one’s module imports.
Every NgModule-based app has at least one module — the root module, conventionally AppModule — which Angular boots from main.ts.
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { UserCardComponent } from './user-card.component';
import { TruncatePipe } from './truncate.pipe';
@NgModule({
declarations: [AppComponent, UserCardComponent, TruncatePipe],
imports: [BrowserModule, FormsModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
// main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
The four metadata arrays
| Property | Purpose | Typical contents |
|---|---|---|
declarations | Registers components, directives, and pipes that belong to this module | AppComponent, TruncatePipe |
imports | Pulls in other NgModules whose exported declarables/providers you need | BrowserModule, RouterModule, HttpClientModule |
exports | Re-publishes declarables (or imported modules) so consumers of this module can use them | UserCardComponent, CommonModule |
providers | Registers injectables (services) into the injector | UserService, interceptors |
There are two more you’ll meet: bootstrap (root module only — the component Angular renders into index.html) and entryComponents (legacy, removed in modern versions for dynamically created components).
A declarable belongs to exactly one NgModule. Listing the same component in two
declarationsarrays throwsType X is part of the declarations of 2 modules. To share it, declare it once andexportit.
Feature modules and sharing
Real apps split functionality into feature modules to keep the root module thin and enable lazy loading. A feature module declares its own components and exports the ones other parts of the app need.
// users/users.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { UserListComponent } from './user-list.component';
import { UserDetailComponent } from './user-detail.component';
const routes: Routes = [
{ path: '', component: UserListComponent },
{ path: ':id', component: UserDetailComponent },
];
@NgModule({
declarations: [UserListComponent, UserDetailComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
exports: [UserListComponent],
})
export class UsersModule {}
Note CommonModule rather than BrowserModule: BrowserModule provides browser-specific services and must be imported only once, in the root module. Feature modules import CommonModule to get NgIf, NgForOf, and the async pipe.
A shared module is a common pattern that bundles widely used declarables and re-exports them, so each feature module imports one thing instead of ten.
// shared/shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TruncatePipe } from './truncate.pipe';
@NgModule({
declarations: [TruncatePipe],
imports: [CommonModule, FormsModule],
exports: [CommonModule, FormsModule, TruncatePipe],
})
export class SharedModule {}
Providers and lazy loading
Services listed in providers are added to an injector. For an eagerly loaded module, those providers all land in the single root injector, so the module boundary doesn’t actually scope them. For a lazy-loaded module, Angular creates a child injector, and that module’s providers become private to it. This is why the modern recommendation is providedIn: 'root' on the service itself — tree-shakable and unambiguous.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly users = ['Ada', 'Linus', 'Grace'];
list(): string[] {
return this.users;
}
}
If you lazy-load UsersModule via the router, you’ll see the chunk load on demand:
Output:
Initial chunk files | Names | Raw size
main.js | main | 142.18 kB
Lazy chunk files | Names | Raw size
users-module.js | users-module | 11.43 kB
Best Practices
- Import
BrowserModuleexactly once, in the root module; everywhere else importCommonModule. - Declare each component, directive, or pipe in only one module, and
exportit when others need it. - Use
providedIn: 'root'for services instead of stuffing them intoprovidersarrays — it’s tree-shakable and avoids duplicate-instance bugs. - Keep a thin
SharedModulefor reusable declarables; never put providers with state in it (they’d be re-instantiated per lazy injector). - Use
RouterModule.forRoot()only in the app’s routing module andforChild()in feature modules. - For new code, prefer standalone components — they remove the boilerplate of NgModules entirely while keeping the same compilation guarantees.