Skip to main content

What are modules in Angular and how are they used?

Angular modules (NgModules) are containers that group components, directives, pipes, and services, wiring them together through the @NgModule decorator.

Theory

TL;DR

  • An NgModule bundles related pieces: it declares what it owns, imports what it needs, and exports what others can use
  • Every app has one root module (AppModule) that bootstraps the app; all other modules are optional and domain-specific
  • Eager modules load upfront with the app; lazy modules load on demand, which cuts the initial bundle size
  • Angular 14+ introduced standalone components that skip NgModules entirely by declaring imports directly on the component
  • Decision rule: one root module plus feature modules per domain; lazy-load routes that are more than two screens deep

Quick example

This is the minimum wiring for any Angular app:

ts
// app.module.ts - bootstraps the application import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], // AppComponent belongs to this module imports: [BrowserModule], // BrowserModule provides DOM rendering + re-exports CommonModule providers: [], // App-wide services bootstrap: [AppComponent] // Component rendered on startup }) export class AppModule {}

Remove this file and the app won't start. These 10 lines are the minimum Angular needs to render anything.

The five fields

@NgModule takes one metadata object with five fields. Mixing them up is the most common mistake in Angular interviews:

  • declarations - components, directives, and pipes that belong to this module. Private unless exported.
  • imports - other modules whose exports you want to use here.
  • exports - your declarations exposed to any module that imports this one.
  • providers - services registered in this module's injector.
  • bootstrap - the root component rendered on startup. Only AppModule needs this.

A component declared in one module is invisible to another unless it's exported from the first and that first module is imported by the second. This is the rule that trips most people up.

Module types

TypePurpose
AppModuleRoot module, bootstraps the app
Feature moduleIsolates one domain: user, admin, checkout
Shared moduleRe-exports common utilities like CommonModule, shared pipes, UI components
Core moduleSingleton services, HTTP interceptors, app-level guards
Lazy moduleFeature loaded on demand via the router

How lazy loading works

Add loadChildren to a route and Angular's router creates a separate JS chunk at build time. The chunk is only fetched when the user navigates to that path.

ts
// app-routing.module.ts const routes: Routes = [ { path: 'admin', // Webpack splits this into a separate admin.js chunk - fetched only on /admin loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) } ];

The lazy module gets its own child injector, parented to the root injector. Services in the lazy module's providers array are scoped to that module only. I've seen this trip up teams who expected a lazy service to be a singleton but got a fresh instance on every navigation.

Standalone components (Angular 14+)

Standalone components declare their own imports directly, with no module required:

ts
// No module needed - the component manages its own dependencies @Component({ standalone: true, imports: [CommonModule, ReactiveFormsModule], template: `<form [formGroup]="form">...</form>` }) export class UserProfileComponent {}

For Angular 17+ projects, standalone is the default. NgModules remain the standard in large legacy codebases and in libraries like Angular Material.

Common mistakes

  1. Declaring one component in two modules. Angular throws at compile time: "Type X is part of the declarations of 2 modules."

    ts
    // Wrong: UserCard declared in both UserModule and SharedModule // Fix: declare in UserModule, re-export from SharedModule exports: [UserCard]
  2. Using BrowserModule in a feature module. It belongs only in AppModule. Feature modules use CommonModule.

    ts
    // Wrong in any module except AppModule: imports: [BrowserModule] // Fix: imports: [CommonModule]
  3. Singleton services in a lazy module's providers array. Each load creates a new instance, losing state.

    ts
    // Wrong - new UserService instance on every navigation: providers: [UserService] // Fix: @Injectable({ providedIn: 'root' }) export class UserService {}
  4. Using RouterModule.forRoot() in a feature module. Use forRoot() once in AppModule; use forChild() everywhere else. Using forRoot() twice silently breaks route guards.

  5. Forgetting CommonModule in feature modules. *ngIf and *ngFor stop working with a confusing error message about unknown directives.

Real-world usage

  • Angular Material: separate module per component (MatButtonModule, MatDialogModule). Import only what you actually use.
  • NGXS / NgRx: call .forRoot() in AppModule, .forFeature() in lazy modules. Mixing these breaks state.
  • Nx workspace: auto-generates lazy feature modules per library. Each lib maps to one bounded context.
  • Core vs Shared pattern: CoreModule holds HTTP_INTERCEPTORS, AuthGuard, global error handlers (imported once in AppModule). SharedModule holds CommonModule, shared pipes, UI wrapper components (imported in every feature module).

Follow-up questions

Q: What is the difference between declarations, imports, and exports in @NgModule?
A: declarations registers components, pipes, and directives as owned by this module (local scope only). imports pulls in another module's exported items for use here. exports makes your declarations visible to any module that imports yours.

Q: Why use CommonModule in feature modules instead of BrowserModule?
A: BrowserModule registers app-level providers that must only exist once. Using it in a feature module causes Angular to throw "BrowserModule has already been loaded." Feature modules get *ngIf and *ngFor by importing CommonModule directly.

Q: How does lazy loading reduce bundle size?
A: loadChildren with a dynamic import() tells webpack to create a separate chunk for that module. The chunk is excluded from the main bundle and fetched only when the router activates that route.

Q: When would you skip NgModules entirely?
A: In Angular 15+ projects using standalone components. Call bootstrapApplication() with an ApplicationConfig, and each component declares its own imports. No AppModule needed.

Q: (Senior) How does the injector hierarchy behave with lazy modules?
A: A lazy module creates a child injector parented to the root injector. When a component inside the lazy module requests a service, Angular searches up: child injector first, then root. A service in the lazy module's providers is a different instance from the one in the root. That is why providedIn: 'root' is the safe default for anything that should be a singleton.

Examples

Root module setup

The minimum to start an Angular app:

ts
// app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, // Only here - sets up DOM rendering, re-exports CommonModule AppRoutingModule // Router setup ], bootstrap: [AppComponent] }) export class AppModule {}

BrowserModule goes here and nowhere else. Any feature module that also imports it will break the app on startup.

Feature module with routing

A user profile domain with reactive forms, following the Angular Material docs structure:

ts
// user.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { UserProfileComponent } from './user-profile.component'; import { UserRoutingModule } from './user-routing.module'; @NgModule({ declarations: [UserProfileComponent], imports: [ CommonModule, // *ngIf, *ngFor, AsyncPipe ReactiveFormsModule, // FormGroup, FormControl UserRoutingModule // Registers /profile route ] // No exports - this module is self-contained }) export class UserModule {} // user-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: 'profile', component: UserProfileComponent } ]; @NgModule({ imports: [RouterModule.forChild(routes)], // forChild, not forRoot exports: [RouterModule] }) export class UserRoutingModule {}

forChild is not optional here. Using forRoot in a feature module causes route guard failures that are hard to trace back to this line.

Shared module pattern

When multiple feature modules need the same pipes and components:

ts
// shared.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DateFormatPipe } from './pipes/date-format.pipe'; import { LoadingSpinnerComponent } from './loading-spinner.component'; @NgModule({ declarations: [DateFormatPipe, LoadingSpinnerComponent], imports: [CommonModule], exports: [ CommonModule, // Feature modules get *ngIf etc. for free DateFormatPipe, LoadingSpinnerComponent ] }) export class SharedModule {} // Any feature module now gets all three with one line: // imports: [SharedModule]

This is the real-world CoreModule / SharedModule split you will find in most production Angular repos. CoreModule holds singletons. SharedModule holds reusable UI pieces.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?