Modules
Understand how Angular organizes features and dependencies using standalone APIs and how NgModules declare, import, export, provide, and bootstrap application functionality in existing module-based projects.
Lesson content
What is a Module? A module groups related application functionality into a clear and reusable unit. In modern Angular applications, features are commonly organized with standalone components, directives, pipes, routes, and providers. Older and existing Angular applications may use NgModule classes to organize declarations, imports, exports, providers, and application bootstrapping. Simple Definition Module = Related Features + Dependencies + Public API + Providers Modern Angular Organization Modern Angular does not require an NgModule for every feature. A standalone component can directly import the components, directives, pipes, and NgModules required by its template. import { Component } from '@angular/core'; import { CurrencyPipe } from '@angular/common'; import { ProductCardComponent } from './product-card.component'; @Component({ selector: 'app-product-list', imports: [ CurrencyPipe, ProductCardComponent ], template: ` @for ( product of products; track product.id ) { <app-product-card [product]="product" /> <p> {{ product.price | currency:'USD' }} </p> } ` }) export class ProductListComponent { products = [ { id: 1, name: 'Keyboard', price: 50 } ]; } Standalone Dependency Flow Standalone Component ↓ imports Array ↓ Components Directives Pipes Existing NgModules ↓ Available in Template What is an NgModule? An NgModule is a TypeScript class decorated with @NgModule() . Its metadata tells Angular which declarations belong together, which dependencies they can use, which declarations are public, and which providers should be registered. import { NgModule } from '@angular/core'; @NgModule({ declarations: [], imports: [], exports: [], providers: [] }) export class UserModule {} Main NgModule Metadata Property Purpose declarations Registers non-standalone components, directives, and pipes owned by the module imports Makes exported features from other modules available exports Makes selected declarations or imported features available to other modules providers Registers injectable dependencies bootstrap Defines the root component for a module-bootstrapped application schemas Configures accepted custom elements or unknown properties 1. declarations The declarations array contains non-standalone components, directives, and pipes that belong to an NgModule. import { NgModule } from '@angular/core'; import { UserCardComponent } from './user-card.component'; import { HighlightDirective } from './highlight.directive'; import { InitialsPipe } from './initials.pipe'; @NgModule({ declarations: [ UserCardComponent, HighlightDirective, InitialsPipe ] }) export class UserModule {} Important Rule A non-standalone component, directive, or pipe can belong to only one NgModule. Declarable ↓ Exactly One NgModule Non-Standalone Component import { Component } from '@angular/core'; @Component({ selector: 'app-user-card', standalone: false, template: ` <h2>User Card</h2> ` }) export class UserCardComponent {} A standalone component is imported rather than declared. 2. imports The imports array contains NgModules or standalone dependencies whose exported features are required by the current module. import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgModule } from '@angular/core'; @NgModule({ imports: [ CommonModule, FormsModule ] }) export class UserModule {} Importing a Standalone Component into an NgModule @NgModule({ imports: [ ProductCardComponent ] }) export class ProductModule {} Standalone components, directives, and pipes belong in imports , not declarations . 3. exports The exports array defines the public template API of an NgModule. A declared component remains private to its module unless it is exported. @NgModule({ declarations: [ UserCardComponent ], exports: [ UserCardComponent ] }) export class UserModule {} Export Flow UserModule ↓ Declares UserCardComponent ↓ Exports UserCardComponent ↓ Another Module Imports UserModule ↓ UserCardComponent Available Re-Exporting Modules An NgModule can import and export another module. import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgModule } from '@angular/core'; @NgModule({ imports: [ CommonModule, FormsModule ], exports: [ CommonModule, FormsModule ] }) export class SharedModule {} Any NgModule importing SharedModule can use the exported functionality. 4. providers The providers array registers dependencies with the module injector. import { NgModule } from '@angular/core'; @NgModule({ providers: [ UserService ] }) export class UserModule {} Services provided by an eagerly imported NgModule may become available broadly in the application injector hierarchy. For most application-wide services, prefer: @Injectable({ providedIn: 'root' }) export class UserService {} 5. bootstrap The bootstrap array identifies the root component when an application starts through an NgModule. import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], bootstrap: [ AppComponent ] }) export class AppModule {} Module Bootstrap import { platformBrowser } from '@angular/platform-browser'; platformBrowser() .bootstrapModule(AppModule) .catch(error => console.error(error) ); Bootstrap Flow main.ts ↓ bootstrapModule(AppModule) ↓ AppModule Metadata ↓ AppComponent ↓ Application Starts Standalone Application Bootstrap Modern standalone applications commonly start with bootstrapApplication() . import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { appConfig } from './app.config'; bootstrapApplication( AppComponent, appConfig ).catch(error => console.error(error) ); Application Configuration import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes) ] }; Standalone vs NgModule Standalone Approach NgModule Approach Dependencies imported by components Dependencies grouped through modules No declarations array Uses declarations for non-standalone declarables Bootstraps with bootstrapApplication() Can bootstrap with bootstrapModule() Recommended for new code Common in existing applications and libraries Direct and explicit dependencies Module-level dependency scope Can import existing NgModules Can import standalone dependencies Standalone Component Import Rules A standalone component lists its template dependencies in its own imports array. @Component({ selector: 'app-dashboard', imports: [ RouterLink, AsyncPipe, UserCardComponent ], template: ` <a routerLink="/users"> Users </a> <app-user-card /> ` }) export class DashboardComponent {} Only dependencies used by the component template need to be imported. Feature Organization Without NgModules A standalone feature can be organized through files, folders, routes, and providers. users/ │ ├── user.routes.ts ├── user.service.ts ├── user-list.component.ts ├── user-card.component.ts ├── user-form.component.ts └── user.model.ts Feature Routes import { Routes } from '@angular/router'; export const USER_ROUTES: Routes = [ { path: '', loadComponent: () => import( './user-list.component' ).then( module => module.UserListComponent ) }, { path: 'create', loadComponent: () => import( './user-form.component' ).then( module => module.UserFormComponent ) } ]; Lazy-Load Feature Routes export const routes: Routes = [ { path: 'users', loadChildren: () => import( './users/user.routes' ).then( module => module.USER_ROUTES ) } ]; Feature NgModule An existing module-based application may group related feature declarations into a feature NgModule. import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [ UserListComponent, UserFormComponent ], imports: [ CommonModule, ReactiveFormsModule, UserRoutingModule ] }) export class UserModule {} Shared NgModule A shared module groups reusable UI declarations used by multiple module-based features. @NgModule({ declarations: [ ButtonComponent, CardComponent, InitialsPipe ], imports: [ CommonModule ], exports: [ ButtonComponent, CardComponent, InitialsPipe ] }) export class SharedModule {} Shared Module Guidelines Include reusable UI features Export only functionality other modules need Avoid feature-specific business logic Avoid application-wide state providers Keep the module focused Core NgModule Some existing applications use a core module for application-wide infrastructure. @NgModule({ providers: [ AuthService, LoggingService, ErrorHandlerService ] }) export class CoreModule {} In standalone applications, equivalent providers commonly belong in the application configuration. export const appConfig: ApplicationConfig = { providers: [ AuthService, LoggingService, ErrorHandlerService ] }; Routing NgModule A module-based feature may place routing configuration in a separate routing module. import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: '', component: UserListComponent }, { path: 'create', component: UserFormComponent } ]; @NgModule({ imports: [ RouterModule.forChild( routes ) ], exports: [ RouterModule ] }) export class UserRoutingModule {} RouterModule.forRoot() Use RouterModule.forRoot() once in the root module of a module-based application. @NgModule({ imports: [ RouterModule.forRoot( routes ) ], exports: [ RouterModule ] }) export class AppRoutingModule {} RouterModule.forChild() Use RouterModule.forChild() for feature routing modules. @NgModule({ imports: [ RouterModule.forChild( userRoutes ) ], exports: [ RouterModule ] }) export class UserRoutingModule {} forRoot() and forChild() forRoot() forChild() Used once at application root Used in feature modules Configures root services and routes Adds feature configuration Creates application-level providers Usually reuses existing root services ModuleWithProviders Pattern A configurable NgModule can expose a static forRoot() method that returns the module with providers. import { ModuleWithProviders, NgModule } from '@angular/core'; export interface LoggerConfig { enabled: boolean; } export const LOGGER_CONFIG = new InjectionToken<LoggerConfig>( 'logger.config' ); @NgModule() export class LoggerModule { static forRoot( config: LoggerConfig ): ModuleWithProviders<LoggerModule> { return { ngModule: LoggerModule, providers: [ { provide: LOGGER_CONFIG, useValue: config } ] }; } } Usage @NgModule({ imports: [ LoggerModule.forRoot({ enabled: true }) ] }) export class AppModule {} Environment Providers Standalone applications commonly configure framework services using provider functions. import { ApplicationConfig } from '@angular/core'; import { provideHttpClient } from '@angular/common/http'; import { provideRouter } from '@angular/router'; export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), provideRouter(routes) ] }; Provider Function Flow ApplicationConfig ↓ Provider Functions ↓ Environment Injector ↓ Dependencies Available Using Existing NgModules in Standalone Components Standalone components can import existing NgModules. import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-name-editor', imports: [ FormsModule ], template: ` <input [(ngModel)]="name" /> ` }) export class NameEditorComponent { name = ''; } This allows standalone and module-based code to work together during incremental migration. Using Standalone Components in NgModules An NgModule can import and export standalone components. @NgModule({ imports: [ ProductCardComponent ], exports: [ ProductCardComponent ] }) export class ProductModule {} Lazy Loading an NgModule Existing module-based features can be lazy-loaded through the router. export const routes: Routes = [ { path: 'admin', loadChildren: () => import( './admin/admin.module' ).then( module => module.AdminModule ) } ]; Lazy Module Flow User Opens /admin ↓ Router Matches Route ↓ AdminModule Downloaded ↓ Feature Dependencies Created ↓ Admin Route Displayed Eager vs Lazy Modules Eager Loading Lazy Loading Loaded during application startup Loaded when its route is requested Available immediately Can reduce initial bundle work Useful for essential features Useful for larger optional features BrowserModule vs CommonModule BrowserModule CommonModule Used by a root browser NgModule Used by feature NgModules Provides browser application infrastructure Exports common directives and pipes Normally imported once Can be imported by many feature modules Declarations vs Imports vs Exports declarations imports exports What the module owns What the module uses What the module shares Non-standalone declarables Modules and standalone dependencies Public declarations and imported dependencies Private unless exported Makes dependencies available internally Makes dependencies available externally Easy Memory Formula Declarations = I Own It Imports = I Need It Exports = I Share It Providers = I Supply It Bootstrap = I Start It Common Errors 1. Declaring a Standalone Component Avoid: @NgModule({ declarations: [ ProductCardComponent ] }) When ProductCardComponent is standalone, import it instead. Prefer: @NgModule({ imports: [ ProductCardComponent ] }) 2. Declaring the Same Component Twice A non-standalone declarable can belong to only one NgModule. 3. Forgetting to Export a Shared Component Declaring a component does not automatically make it available to importing modules. 4. Importing BrowserModule in Feature Modules Use BrowserModule in the root browser module and CommonModule in feature NgModules. 5. Adding Every Service to providers Prefer providedIn: 'root' for application-wide services unless a narrower or configurable scope is required. 6. Creating Large Shared Modules A large shared module can hide dependencies and include functionality that consumers do not need. Prefer focused standalone imports or small, cohesive shared modules. 7. Using NgModules Automatically for New Features For new Angular code, start with standalone components, routes, and providers unless an NgModule is required for compatibility or library integration. Migrating from NgModules to Standalone Existing applications can migrate incrementally. Typical Migration Flow Existing NgModule Application ↓ Convert Declarables to Standalone ↓ Move Template Dependencies to Component imports ↓ Replace Module Bootstrap with bootstrapApplication() ↓ Move Providers to appConfig ↓ Remove Unnecessary NgModules Migration Command ng generate @angular/core:standalone Run migrations on a clean source-control branch and verify the application after every step. Best Practices Prefer standalone components, directives, and pipes for new code Use NgModules when maintaining existing module-based applications Keep feature boundaries based on business functionality Import only the dependencies a component or feature needs Export only the public API that consumers require Keep shared modules small and cohesive Prefer provider functions in standalone application configuration Use lazy loading for large optional features Do not declare standalone components in an NgModule Avoid duplicating application-wide providers Migrate existing applications incrementally rather than rewriting everything at once Interview Questions What is an Angular module? A module is a logical grouping of related application functionality, dependencies, providers, and public APIs. What is an NgModule? An NgModule is a class decorated with @NgModule() that configures declarations, imports, exports, providers, and optional bootstrapping. Are NgModules required for new Angular applications? No. New applications can be built using standalone components, directives, pipes, routes, and provider functions. What belongs in declarations? Only non-standalone components, directives, and pipes owned by that NgModule belong in declarations . What belongs in imports? NgModules and standalone components, directives, or pipes required by the current module belong in imports . What is the difference between imports and exports? imports makes dependencies available inside a module. exports makes selected functionality available to modules that import it. Can a standalone component import an NgModule? Yes. A standalone component can import existing NgModules through its imports array. Can an NgModule use a standalone component? Yes. The NgModule imports the standalone component instead of declaring it. What is the difference between forRoot and forChild? forRoot() commonly configures application-level services and root configuration once. forChild() adds feature configuration without recreating root services. What is a SharedModule? A SharedModule is an NgModule that exports reusable components, directives, pipes, or other template dependencies to module-based features. What is a feature module? A feature module groups functionality belonging to a particular business feature, workflow, or application area. What is lazy loading? Lazy loading downloads and initializes a feature only when its route or functionality is requested. Key Point Modern Angular organizes new code with standalone dependencies, while NgModules remain important for understanding, maintaining, and integrating existing module-based applications. Easy Formula Standalone Component = Component + Direct Imports + Explicit Dependencies NgModule = Declarations + Imports + Exports + Providers + Optional Bootstrap declarations = What I Own imports = What I Use exports = What I Share providers = What I Inject bootstrap = What Starts the App