1. What is an NgModule
An NgModule groups related components, directives, and pipes together. It was the primary way to structure Angular apps before standalone components arrived in Angular 14.
NgModules are not deprecated — they are still fully supported in Angular 21. Only platformBrowserDynamic (the old bootstrap function) was deprecated. The recommended path for new projects is standalone components, but NgModules remain valid for existing codebases.
2. NgModule Anatomy
The @NgModule decorator accepts five main properties:
| Property | Purpose |
|---|---|
declarations |
Non-standalone components, directives, and pipes owned by this module |
imports |
Other NgModules or standalone components/pipes this module needs |
exports |
What this module exposes to other modules that import it |
providers |
Services to register (prefer providedIn: 'root' on the service itself) |
bootstrap |
The root component Angular renders on startup — root module only |
@NgModule({
declarations: [App],
bootstrap: [App],
imports: [BrowserModule, CommonModule, Tasks, NewTask, Task, User, Header, RouterOutlet],
})
export class AppModule {}
3. declarations vs imports
This is the most common source of confusion when mixing modules with standalone components.
-
declarations— only for non-standalone components (standalone: false). They are private to this module unless explicitly exported. -
imports— for other NgModules and for standalone components, directives, and pipes (the default since Angular 17).
Putting a standalone component in declarations causes a compile error. Putting a non-standalone component in imports also causes an error.
// non-standalone component — belongs in declarations
@Component({
standalone: false,
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}
// standalone component (Angular 17+ default) — belongs in imports, not declarations
@Component({
selector: 'app-header',
templateUrl: './header.html',
})
export class Header {}
@NgModule({
declarations: [App], // non-standalone
imports: [BrowserModule, Header, RouterOutlet], // standalone components go here
bootstrap: [App],
})
export class AppModule {}
A non-standalone component also cannot have an imports array in @Component — it delegates all imports to its module.
4. BrowserModule vs CommonModule
| Module | Use when |
|---|---|
BrowserModule |
Root module only — sets up the browser platform, includes CommonModule internally |
CommonModule |
Feature modules — provides NgIf, NgFor, NgClass, pipes, etc. |
Never import BrowserModule in a feature module — import CommonModule instead. Importing BrowserModule twice throws a runtime error.
5. Bootstrapping with NgModule
Use platformBrowser().bootstrapModule() from @angular/platform-browser. The older platformBrowserDynamic from @angular/platform-browser-dynamic was deprecated in Angular 21.
// main.ts
import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app/app.module';
platformBrowser().bootstrapModule(AppModule)
.catch((err: unknown) => console.error(err));
6. Feature Modules
Feature modules group a slice of the app (e.g. tasks, users) to keep AppModule small. They export only what the rest of the app needs.
// tasks/tasks.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TasksComponent } from './tasks.component';
@NgModule({
declarations: [TasksComponent],
imports: [CommonModule],
exports: [TasksComponent],
})
export class TasksModule {}
// app.module.ts
@NgModule({
declarations: [App],
imports: [BrowserModule, TasksModule],
bootstrap: [App],
})
export class AppModule {}
7. Lazy-Loaded Feature Modules
Lazy loading defers loading a module's bundle until the user navigates to its route. Configure it with loadChildren in the router:
// app.routes.ts
export const routes: Routes = [
{
path: 'tasks',
loadChildren: () =>
import('./tasks/tasks.module').then((m) => m.TasksModule),
},
];
// tasks/tasks.module.ts — use forChild, not forRoot
@NgModule({
declarations: [TasksComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: TasksComponent }]),
],
})
export class TasksModule {}
The root module uses RouterModule.forRoot(routes). Feature modules use RouterModule.forChild(routes).
8. NgModule vs Standalone
| NgModule | Standalone | |
|---|---|---|
| Grouping | Explicit module files | No module needed |
| Sharing | Export from module, import the module | Import the component directly |
| Lazy loading | loadChildren |
loadComponent |
| Recommended for new projects | No | Yes |
| Deprecated | No | — |
NgModules are still valid and fully supported. Understanding them is essential for working with existing Angular codebases.
Top comments (0)