Angular 18 🅰️ Modules and NgModule
NgModules are Angular’s original way to organize code — a class decorated with @NgModule that groups components, directives, pipes, and services into a cohesive block. For most of Angular’s history, every app had at least one module, and libraries shipped as modules. Since Angular 14, standalone components have offered an alternative: components that declare their own dependencies without a module. Angular 19 made standalone the default. NgModules still work and remain everywhere in existing code, but new code should be standalone unless there’s a specific reason to use a module. This chapter covers both — because you’ll encounter NgModules in real projects for years, and understanding them explains why standalone exists.
Key point: An NgModule is a class with @NgModule that declares what belongs to it (declarations), what it needs from other modules (imports), what it exposes to other modules (exports), and what services it provides (providers). A standalone component skips the module entirely — it imports its dependencies directly. Standalone is the modern default; NgModules remain for existing code, libraries that haven’t migrated, and a few scenarios where a module still helps.
What an NgModule is
An NgModule is a class decorated with @NgModule that groups related code.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserListComponent } from './user-list.component';
import { UserCardComponent } from './user-card.component';
@NgModule({
declarations: [UserListComponent, UserCardComponent],
imports: [CommonModule],
exports: [UserListComponent]
})
export class UserModule {}
The decorator’s metadata describes the module:
| Property | Purpose |
|---|---|
declarations | Components, directives, pipes owned by this module |
imports | Other modules this module needs |
exports | What this module exposes to other modules |
providers | Services this module provides |
bootstrap | Root component (root module only) |
What an NgModule does:
- Groups related code into a cohesive unit
- Provides a compilation context — declared components can use each other in templates
- Controls visibility — only exported items are usable outside
- Registers providers — services available in the injector
- Enables lazy loading — modules can be loaded on demand
The classic root module:
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
AppModule bootstraps the app. Its bootstrap array names the root component.
What an NgModule is not:
- Not a JavaScript module — it doesn’t map to
import/export - Not a class instantiated at runtime — Angular reads the metadata
- Not required — standalone components skip it entirely
Why NgModules exist: They were Angular’s answer to organizing large apps. Before standalone components, every component had to be declared in a module. Modules provided compilation contexts, controlled visibility, and enabled lazy loading. They’re still functional; they’re just no longer required.
Why standalone replaced them: NgModules added ceremony. A component couldn’t exist without being declared in a module, which meant every new component touched two files. Standalone components declare their dependencies directly — one file, no module. Angular’s team found that most apps didn’t benefit from the module grouping; they just paid the tax. Standalone removes it.
declarations — components, directives, pipes
declarations lists what the module owns.
@NgModule({
declarations: [
UserListComponent,
UserCardComponent,
HighlightDirective,
CurrencyFormatPipe
]
})
export class UserModule {}
What can be declared:
- Components
- Directives
- Pipes
What can’t be declared:
- Other modules (use
imports) - Services (use
providers) - Standalone components (they declare their own imports)
The rule: Every component, directive, and pipe belongs to exactly one module — unless it’s standalone.
// ✅ Declared in one module
@NgModule({ declarations: [UserCardComponent] })
export class UserModule {}
// ❌ Declared in two modules
@NgModule({ declarations: [UserCardComponent] })
export class OtherModule {}
// Error: Type UserCardComponent is part of the declarations of 2 modules.
Declared items can use each other’s templates:
// user-list.component.html
<app-user-card [user]="user"></app-user-card>
UserCardComponent is available in UserListComponent‘s template because both are declared in the same module.
What declarations do not do:
- They don’t make components available outside the module (that’s
exports) - They don’t provide services
- They don’t set up routing
Why declarations are module-scoped: Angular needs a compilation context to know which components can use which directives and pipes. The module provides it. A component can only use what’s declared in its module or imported from another.
Why “declarations” and not “components”: The array includes components, directives, and pipes — three kinds of things. “Declarations” is the general term. A module “declares” what belongs to it.
imports — modules to use
imports lists the modules this module needs.
@NgModule({
declarations: [UserListComponent],
imports: [
CommonModule,
FormsModule,
UserModule
]
})
export class AdminModule {}
What goes in imports:
- Other NgModules
- Standalone components, directives, and pipes (since Angular 14)
Common modules:
| Module | Provides |
|---|---|
BrowserModule | Core browser services (root only) |
CommonModule | *ngIf, *ngFor, built-in pipes |
FormsModule | ngModel, template-driven forms |
ReactiveFormsModule | Form groups, controls |
HttpClientModule | HTTP client (legacy) |
RouterModule | Routing directives |
A module’s imports make their exports available to its declarations:
// UserModule exports UserListComponent
@NgModule({
declarations: [UserListComponent],
exports: [UserListComponent]
})
export class UserModule {}
// AdminModule imports UserModule
@NgModule({
declarations: [AdminDashboardComponent],
imports: [UserModule]
})
export class AdminModule {}
// AdminDashboardComponent can use <app-user-list>
Standalone components in imports:
import { UserCardComponent } from './user-card.component';
@NgModule({
declarations: [UserListComponent],
imports: [CommonModule, UserCardComponent]
})
export class UserModule {}
A module can import a standalone component. The module’s declared components can then use it.
BrowserModule vs CommonModule:
BrowserModule— for the root module only. It configures browser-specific services.CommonModule— for feature modules. It provides*ngIf,*ngFor, and pipes.
Importing BrowserModule in a feature module is an error.
Why imports matter: They’re the module’s dependencies. A module declares its components, but those components often need directives and pipes from other modules. imports brings them in.
Why
CommonModuleis separate fromBrowserModule:BrowserModuleincludes everythingCommonModuledoes, plus browser-specific setup. Feature modules don’t need the browser setup — that’s done once in the root module. SoCommonModuleis the lighter version for features.
exports — making items available
exports lists what the module makes available to other modules.
@NgModule({
declarations: [UserListComponent, UserCardComponent],
imports: [CommonModule],
exports: [UserListComponent]
})
export class UserModule {}
Only UserListComponent is exported. UserCardComponent is used internally but hidden from consumers.
What can be exported:
- Declared components, directives, pipes
- Imported modules (re-exporting them)
Re-exporting a module:
@NgModule({
exports: [CommonModule, FormsModule]
})
export class SharedModule {}
SharedModule re-exports CommonModule and FormsModule. A module that imports SharedModule gets all three.
The SharedModule pattern:
@NgModule({
declarations: [
HighlightDirective,
TruncatePipe,
ButtonComponent
],
imports: [CommonModule],
exports: [
HighlightDirective,
TruncatePipe,
ButtonComponent,
CommonModule
]
})
export class SharedModule {}
SharedModule bundles reusable components, directives, and pipes — plus CommonModule itself — so consumers import one module and get everything.
Why nothing is exported by default: Encapsulation. A module is a boundary; exports control the public API. Components used internally stay internal. Only what’s explicitly exported is visible.
What exports doesn’t do:
- It doesn’t provide services (that’s
providers) - It doesn’t automatically export declarations of imported modules
- It doesn’t re-export imports unless you list them
Why exports matter: They’re the module’s public interface. Everything declared but not exported is private to the module. A well-designed module exports only what consumers need.
Why re-exporting
CommonModuleis common: Every feature module needs*ngIfand*ngFor. Without re-exporting, each feature would importCommonModuleitself. TheSharedModulepattern centralizes this — import one module, get the common set.
providers — services
providers lists services this module provides.
@NgModule({
declarations: [UserListComponent],
providers: [UserService, LoggerService]
})
export class UserModule {}
What goes in providers:
- Services (classes with
@Injectable) - Injection tokens
- Provider objects (
{ provide, useClass })
Modern alternative — providedIn: 'root':
@Injectable({ providedIn: 'root' })
export class UserService {}
providedIn: 'root' makes the service a singleton available everywhere, no module needed. It’s the modern default.
providedIn options:
| Value | Scope |
|---|---|
'root' | App-wide singleton |
'platform' | Across multiple apps on the page |
'any' | New instance per module that uses it |
SomeModule | Scoped to a specific module |
When to use providers in a module:
- When the service is scoped to the module (not app-wide)
- When you need to provide a custom implementation
- When you can’t use
providedIn(some libraries require module-level providers)
Scoping services to a module:
@Injectable()
export class UserService {}
@NgModule({
providers: [UserService]
})
export class UserModule {}
The service is available only to components in modules that import UserModule.
Lazy-loaded module providers: When a module is lazy-loaded, its providers create a separate injector. Services are scoped to that lazy module — a new instance for each lazy load.
Why providers matter: They register services with the injector. Where the provider is declared determines scope — root, module, or component. Modern code prefers providedIn: 'root'; module providers remain for scoped or custom cases.
Why
providedIn: 'root'is preferred: It makes the service tree-shakable — if nothing uses it, it’s dropped from the bundle. Module providers aren’t tree-shakable.providedIn: 'root'is simpler and produces smaller bundles.
The root module and bootstrapping
Every module-based app has a root module. It bootstraps the app.
main.ts:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
platformBrowserDynamic().bootstrapModule(AppModule) starts the app.
app.module.ts:
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
The bootstrap array names the root component.
What goes in the root module:
BrowserModule(required)- The root component in
declarationsandbootstrap - App-wide modules
- App-wide providers (or use
providedIn: 'root')
What doesn’t belong in the root module:
- Feature components (declare them in feature modules)
CommonModule(use it in feature modules)- Feature-specific providers
The AppComponent template hosts the app:
<app-header></app-header>
<router-outlet></router-outlet>
<app-footer></app-footer>
Standalone bootstrapping — the modern alternative:
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error(err));
bootstrapApplication bootstraps a standalone component directly — no module. That’s the default since Angular 19.
Why bootstrapping changed: Standalone components don’t need a module, so bootstrapApplication replaces bootstrapModule. It’s simpler and works with the modern default.
Why the root module is special: It’s the entry point.
BrowserModulecan only be imported once — in the root module. That’s why feature modules useCommonModuleinstead. The root module is the framework’s setup; feature modules are your code.
Feature modules
Feature modules organize code by feature.
Typical feature module:
@NgModule({
declarations: [
UserListComponent,
UserDetailComponent,
UserFormComponent
],
imports: [
CommonModule,
ReactiveFormsModule,
UserRoutingModule
],
providers: [UserService]
})
export class UserModule {}
Everything related to users lives in UserModule.
What a feature module contains:
- Its own components
- Its own services
- Its own routing (via a routing module)
- Its own child modules
Feature module with routing:
// user-routing.module.ts
@NgModule({
imports: [RouterModule.forChild([
{ path: '', component: UserListComponent },
{ path: ':id', component: UserDetailComponent }
])],
exports: [RouterModule]
})
export class UserRoutingModule {}
RouterModule.forChild() is for feature routing. RouterModule.forRoot() is for root routing.
Lazy loading a feature module:
const routes: Routes = [
{
path: 'users',
loadChildren: () => import('./user/user.module').then(m => m.UserModule)
}
];
loadChildren loads the module on demand.
Why feature modules matter: They give each feature a clear boundary. Components, services, and routes for “users” are together. Lazy loading makes features optional to load.
Why feature modules still exist in a standalone world: Lazy loading still uses modules (or
loadComponentfor a single standalone component). Feature modules remain the standard way to bundle related code for lazy loading. Standalone reduces the ceremony for individual components but doesn’t replace the lazy-loading use case.
The SharedModule pattern
A common module that bundles reusable pieces.
@NgModule({
declarations: [
ButtonComponent,
CardComponent,
HighlightDirective,
TruncatePipe,
TimeAgoPipe
],
imports: [CommonModule],
exports: [
ButtonComponent,
CardComponent,
HighlightDirective,
TruncatePipe,
TimeAgoPipe,
CommonModule
]
})
export class SharedModule {}
Why it’s useful: Every feature module needs the common pieces. Instead of importing each, they import SharedModule and get everything.
What belongs in SharedModule:
- Reusable components (buttons, cards, badges)
- Reusable directives
- Reusable pipes
CommonModule(re-exported)
What doesn’t:
- Services (use
providedIn: 'root') - Feature-specific components
- Single-use components
Feature module usage:
@NgModule({
declarations: [ProductListComponent],
imports: [SharedModule, ReactiveFormsModule]
})
export class ProductModule {}
ProductListComponent can use everything from SharedModule.
The risk: SharedModule becomes a dumping ground. Everything ends up in it, and every module imports it. The rule: only genuinely shared pieces belong.
Why SharedModule is common: It reduces boilerplate. Without it, every feature imports CommonModule, FormsModule, and a dozen components. With it, one import.
Why
SharedModulere-exportsCommonModule: The shared components need*ngIfand*ngForfromCommonModule. By re-exporting it, feature modules get both the components andCommonModulein one import. The feature modules don’t need to importCommonModuleseparately.
NgModule vs standalone
The comparison between the two approaches.
| Aspect | NgModule | Standalone |
|---|---|---|
| Component declaration | In declarations | In imports of the component |
| Dependencies | Module imports | Component imports |
| Visibility | Module exports | Explicit export |
| Lazy loading | loadChildren | loadComponent or loadChildren |
| Bootstrapping | bootstrapModule | bootstrapApplication |
| Default since | Pre-19 | Angular 19+ |
| Removed? | No | N/A |
| Recommended | Legacy | Modern |
Standalone component:
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule, RouterLink],
template: `...`
})
export class UserCardComponent {}
The component declares its own dependencies. No module needed.
Module-declared component:
@Component({
selector: 'app-user-card',
template: `...`
})
export class UserCardComponent {}
@NgModule({
declarations: [UserCardComponent],
imports: [CommonModule]
})
export class UserModule {}
The module declares the component and its dependencies.
Migrating from module to standalone:
- Add
standalone: trueto the component - Move the module’s imports into the component’s
imports - Remove the component from
declarations - Delete the module if nothing else uses it
When to still use modules:
- Libraries that haven’t migrated
- Teams not ready to migrate
- The rare case where a module-level provider scope is useful
- Lazy loading large feature bundles
When to use standalone:
- All new code
- Anything that can migrate easily
- Simple apps with few shared pieces
Why standalone is the modern default: Less ceremony, clearer dependencies, easier migration, better tree-shaking. NgModules did their job; standalone does it better for most cases.
Why migration is easy: A module-based component converts to standalone by moving imports from the module into the component. Angular even has automated migrations (
ng generate @angular/core:standalone). Once all components are standalone, the modules can be deleted.
A full example
A module-based app and its standalone equivalent.
Module-based:
// user.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { UserListComponent } from './user-list.component';
import { UserCardComponent } from './user-card.component';
import { UserFormComponent } from './user-form.component';
import { UserService } from './user.service';
@NgModule({
declarations: [
UserListComponent,
UserCardComponent,
UserFormComponent
],
imports: [
CommonModule,
ReactiveFormsModule
],
exports: [
UserListComponent
],
providers: [
UserService
]
})
export class UserModule {}
Components:
// user-list.component.ts
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent {
users$ = inject(UserService).getUsers();
}
// user-card.component.ts
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html'
})
export class UserCardComponent {
@Input() user!: User;
}
Standalone equivalent:
// user-list.component.ts
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule, UserCardComponent],
templateUrl: './user-list.component.html'
})
export class UserListComponent {
users$ = inject(UserService).getUsers();
}
// user-card.component.ts
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule],
templateUrl: './user-card.component.html'
})
export class UserCardComponent {
@Input() user!: User;
}
Service:
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
}
No module. Each component declares its own imports. The service provides itself at root.
What changed:
- No
user.module.ts standalone: trueon each componentimportson each component, not the module- Service uses
providedIn: 'root'instead of moduleproviders
Why this shape: It’s the modern Angular pattern. Each component is self-contained, declaring exactly what it needs. Dependencies are visible at the point of use, not in a module file. The result is fewer files and clearer relationships.
Why it’s shorter: No module file. No declarations array. No exports to maintain. Each component is one file with its own dependencies. That reduction in ceremony is why standalone became the default.
Complete Example Session
# ============================================
# PART 1: ROOT MODULE
# ============================================
cat > app.module.ts << 'EOF'
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 {}
EOF
npx tsc --noEmit app.module.ts
# (no errors)
# ============================================
# PART 2: FEATURE MODULE
# ============================================
cat > user.module.ts << 'EOF'
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserListComponent } from './user-list.component';
import { UserCardComponent } from './user-card.component';
@NgModule({
declarations: [UserListComponent, UserCardComponent],
imports: [CommonModule],
exports: [UserListComponent]
})
export class UserModule {}
EOF
npx tsc --noEmit user.module.ts
# (no errors)
# ============================================
# PART 3: SHARED MODULE
# ============================================
cat > shared.module.ts << 'EOF'
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent } from './button.component';
@NgModule({
declarations: [ButtonComponent],
imports: [CommonModule],
exports: [ButtonComponent, CommonModule]
})
export class SharedModule {}
EOF
npx tsc --noEmit shared.module.ts
# (no errors)
# ============================================
# PART 4: ROUTING MODULE
# ============================================
cat > user-routing.module.ts << 'EOF'
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { UserListComponent } from './user-list.component';
import { UserDetailComponent } from './user-detail.component';
@NgModule({
imports: [RouterModule.forChild([
{ path: '', component: UserListComponent },
{ path: ':id', component: UserDetailComponent }
])],
exports: [RouterModule]
})
export class UserRoutingModule {}
EOF
npx tsc --noEmit user-routing.module.ts
# (no errors)
# ============================================
# PART 5: STANDALONE EQUIVALENT
# ============================================
cat > user-list.component.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserCardComponent } from './user-card.component';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule, UserCardComponent],
template: `
<app-user-card *ngFor="let user of users()" [user]="user" />
`
})
export class UserListComponent {
users = inject(UserService).getUsers;
}
EOF
npx tsc --noEmit user-list.component.ts
# (no errors)
# ============================================
# PART 6: STANDALONE BOOTSTRAP
# ============================================
cat > main.ts << 'EOF'
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient()
]
}).catch(err => console.error(err));
EOF
npx tsc --noEmit main.ts
# (no errors)
# ============================================
# PART 7: MODULE STRUCTURE SUMMARY
# ============================================
cat << 'EOF'
Module-based structure:
app/
├── app.module.ts ← root
├── app.component.ts
├── shared/
│ └── shared.module.ts ← shared pieces
└── user/
├── user.module.ts
├── user-routing.module.ts
└── components...
Standalone structure:
app/
├── app.component.ts ← root
├── app.config.ts ← providers
├── app.routes.ts ← routes
├── shared/
│ └── components... ← no module
└── user/
├── user-list.component.ts
└── components...
Modules group by feature.
Standalone groups by component.
EOF
Quick Reference
@NgModule Properties
| Property | Purpose |
|---|---|
declarations | Components, directives, pipes |
imports | Other modules to use |
exports | What other modules can use |
providers | Services to register |
bootstrap | Root component (root only) |
What Goes Where
| Item | Where |
|---|---|
| Component | declarations |
| Directive | declarations |
| Pipe | declarations |
| Module | imports |
| Standalone component | imports |
| Service | providers or providedIn: 'root' |
| Injection token | providers |
Common Modules
| Module | Provides | For |
|---|---|---|
BrowserModule | Browser setup | Root only |
CommonModule | *ngIf, *ngFor, pipes | Features |
FormsModule | ngModel | Template forms |
ReactiveFormsModule | FormGroup etc. | Reactive forms |
HttpClientModule | HTTP client | Legacy HTTP |
RouterModule | Routing | Root + features |
providedIn Options
| Value | Scope |
|---|---|
'root' | App-wide singleton |
'platform' | Across apps |
'any' | Per module |
SomeModule | Module-scoped |
(no providedIn) | Must be in providers |
Feature Module Structure
| File | Purpose |
|---|---|
feature.module.ts | Module with declarations + imports |
feature-routing.module.ts | Routes for the feature |
| Components | In declarations |
| Service | providedIn: 'root' or in providers |
forRoot vs forChild
| Form | Use |
|---|---|
RouterModule.forRoot(routes) | Root module only |
RouterModule.forChild(routes) | Feature modules |
Lazy Loading
| Approach | Syntax |
|---|---|
| Module | loadChildren: () => import('./m').then(m => m.Module) |
| Standalone | loadComponent: () => import('./c').then(c => c.Component) |
NgModule vs Standalone
| Aspect | NgModule | Standalone |
|---|---|---|
| Component declaration | declarations | imports of component |
| Default since | Pre-19 | Angular 19 |
| Recommended | Legacy | Modern |
| Files per component | 2+ | 1 |
| Removed | No | N/A |
Migration Steps
| Step | Action |
|---|---|
| 1 | Add standalone: true |
| 2 | Move module imports to component |
| 3 | Remove from declarations |
| 4 | Delete module if unused |
| 5 | Use bootstrapApplication |
SharedModule Contents
| Include | Exclude |
|---|---|
| Reusable components | Services |
| Reusable directives | Feature components |
| Reusable pipes | Single-use components |
CommonModule re-export | App-wide modules |
Common Errors
| Error | Cause | Fix |
|---|---|---|
| Component declared twice | In two modules | Declare once |
| Not declared in any module | Missing declaration | Add to a module or make standalone |
BrowserModule twice | Imported in feature | Use CommonModule |
CommonModule missing | Not imported | Add to imports |
| Module imports not available | Not exported | Export the module or item |
Bootstrap Comparison
| Aspect | Module | Standalone |
|---|---|---|
| Entry point | main.ts | main.ts |
| Function | bootstrapModule(AppModule) | bootstrapApplication(AppComponent) |
| Providers | In @NgModule | In second argument |
Best Practices Summary
| Rule | Reason |
|---|---|
| Standalone for new code | Modern default |
| Modules for lazy loading | Still works |
providedIn: 'root' for services | Tree-shakable |
SharedModule for reusable UI | Reduce boilerplate |
CommonModule in features | Not BrowserModule |
| Feature modules for features | Boundary |
| Root module only for bootstrap | BrowserModule once |
Best Practices
✅ Do This:
// Use standalone for new components
@Component({
standalone: true,
imports: [CommonModule, FormsModule]
}) // ✅
// Use providedIn for services
@Injectable({ providedIn: 'root' })
export class UserService {} // ✅
// Use CommonModule in feature modules
@NgModule({
imports: [CommonModule]
}) // ✅
// Use BrowserModule only in root module
@NgModule({
imports: [BrowserModule],
bootstrap: [AppComponent]
}) // ✅
// Re-export CommonModule from SharedModule
@NgModule({
exports: [CommonModule, ButtonComponent]
}) // ✅
// Use RouterModule.forRoot in root
// Use RouterModule.forChild in features // ✅
// Migrate to standalone when practical
// Run the standalone migration // ✅
// Keep the root module minimal
// Bootstrap, BrowserModule, app-wide modules // ✅
// Use loadComponent for standalone lazy loading
loadComponent: () => import('./c').then(c => c.Component) // ✅
❌ Don’t Do This:
// Don't declare a component in two modules
@NgModule({ declarations: [C] })
@NgModule({ declarations: [C] }) // ❌ // ❌
// Don't forget to declare components
@Component({}) // ⚠️ must be in a module or standalone // ⚠️
// Don't use BrowserModule in feature modules
@NgModule({ imports: [BrowserModule] }) // ❌ // ❌
// Don't forget CommonModule for *ngIf / *ngFor
@NgModule({ imports: [] }) // ⚠️ *ngIf won't work // ⚠️
// Don't put everything in SharedModule
// Only genuinely shared pieces // ⚠️
// Don't use forRoot in feature modules
RouterModule.forRoot() in feature module // ❌ // ❌
// Don't put services in declarations
@NgModule({ declarations: [UserService] }) // ❌ // ❌
// Don't export what nobody uses
exports: [InternalComponent] // ⚠️ unnecessary // ⚠️
// Don't mix module and standalone patterns unnecessarily
// Pick one direction for the codebase // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Component in two modules | Compile error | Declare once |
| Not declared anywhere | Template error | Add to module or make standalone |
BrowserModule in feature | Runtime error | Use CommonModule |
Missing CommonModule | *ngIf not found | Add to imports |
Service in declarations | Compile error | Move to providers |
forRoot in feature | Multiple router instances | Use forChild |
SharedModule too big | Imports bloat | Include only shared pieces |
| Lazy module imports feature | Loads unnecessarily | Split modules |
Standalone in declarations | Compile error | Import it instead |
| Migration half-done | Mixed patterns | Migrate component by component |
Real-World Examples
1. Root module
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
bootstrap: [AppComponent]
})
export class AppModule {}
2. Feature module
@NgModule({
declarations: [UserListComponent, UserCardComponent],
imports: [CommonModule, SharedModule],
exports: [UserListComponent]
})
export class UserModule {}
3. Shared module
@NgModule({
declarations: [ButtonComponent, CardComponent],
imports: [CommonModule],
exports: [ButtonComponent, CardComponent, CommonModule]
})
export class SharedModule {}
4. Routing module
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class UserRoutingModule {}
5. Service with providedIn
@Injectable({ providedIn: 'root' })
export class UserService {}
6. Module-scoped service
@Injectable()
export class ScopedService {}
@NgModule({ providers: [ScopedService] })
export class FeatureModule {}
7. Lazy-loaded module
{
path: 'users',
loadChildren: () => import('./user/user.module').then(m => m.UserModule)
}
8. Lazy-loaded standalone
{
path: 'users',
loadComponent: () => import('./user/user-list.component').then(c => c.UserListComponent)
}
9. Standalone component
@Component({
standalone: true,
imports: [CommonModule, FormsModule],
template: `...`
})
export class UserFormComponent {}
10. Standalone app config
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient()
]
};
11. Bootstrap with module
platformBrowserDynamic().bootstrapModule(AppModule);
12. Bootstrap standalone
bootstrapApplication(AppComponent, appConfig);
13. Re-export module
@NgModule({
exports: [CommonModule, FormsModule]
})
export class SharedModule {}
14. Provider with useClass
@NgModule({
providers: [
{ provide: Logger, useClass: ConsoleLogger }
]
})
export class AppModule {}
15. Injection token provider
@NgModule({
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
})
export class AppModule {}
16. SharedModule usage in feature
@NgModule({
declarations: [ProductListComponent],
imports: [SharedModule]
})
export class ProductModule {}
17. Preloading modules
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})
18. Core module pattern
@NgModule({
providers: [AuthService, LoggerService],
imports: [CommonModule]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parent: CoreModule) {
if (parent) throw new Error('CoreModule already loaded');
}
}
19. Migrated standalone component
@Component({
selector: 'app-user',
standalone: true,
imports: [CommonModule, UserCardComponent],
template: `...`
})
export class UserComponent {}
20. Feature module with providers
@NgModule({
declarations: [DashboardComponent],
imports: [CommonModule],
providers: [DashboardStateService]
})
export class DashboardModule {}
Visual: Module Structure
┌──────────────────────────────────────────────┐
│ AppModule (root) │
│ │
│ imports: BrowserModule, AppRoutingModule │
│ bootstrap: [AppComponent] │
│ │
└──────────────────────────────────────────────┘
│
├──► SharedModule
│ exports: ButtonComponent, CardComponent, CommonModule
│
├──► UserModule
│ declarations: UserListComponent, UserCardComponent
│ imports: CommonModule, SharedModule
│
└──► ProductModule
declarations: ProductListComponent
imports: SharedModule
Visual: What Each Property Does
┌──────────────────────────────────────────────┐
│ @NgModule({ │
│ declarations: [C, D, P], │
│ // ─ components, directives, pipes ─ │
│ // ─ owned by this module ─ │
│ │
│ imports: [M1, M2, StandaloneC], │
│ // ─ what this module needs ─ │
│ │
│ exports: [C, M1], │
│ // ─ what consumers can use ─ │
│ │
│ providers: [S1, S2], │
│ // ─ services registered ─ │
│ }) │
│ │
└──────────────────────────────────────────────┘
Visual: Module Compilation Context
┌──────────────────────────────────────────────┐
│ UserModule │
│ │
│ declarations: [UserList, UserCard] │
│ imports: [CommonModule, SharedModule] │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ UserList template │ │
│ │ │ │
│ │ <app-user-card> ✅ declared here │ │
│ │ *ngFor ✅ CommonModule │ │
│ │ <app-button> ✅ SharedModule │ │
│ │ <app-unknown> ❌ not available │ │
│ └────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
Visual: Standalone Alternative
┌──────────────────────────────────────────────┐
│ No module file │
│ │
│ UserListComponent │
│ @Component({ │
│ standalone: true, │
│ imports: [ │
│ CommonModule, ← *ngIf/*ngFor │
│ UserCardComponent, ← declared │
│ ButtonComponent ← shared │
│ ] │
│ }) │
│ │
│ Each component declares its own deps │
│ │
└──────────────────────────────────────────────┘
Visual: providedIn vs Module Providers
┌──────────────────────────────────────────────┐
│ providedIn: 'root' │
│ │
│ @Injectable({ providedIn: 'root' }) │
│ export class UserService {} │
│ │
│ • App-wide singleton │
│ • Tree-shakable │
│ • No module needed │
│ • Modern default │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Module providers │
│ │
│ @NgModule({ │
│ providers: [UserService] │
│ }) │
│ │
│ • Scoped to the module │
│ • Not tree-shakable │
│ • Legacy pattern │
│ • Use when scope matters │
│ │
└──────────────────────────────────────────────┘
Visual: Lazy Loading
┌──────────────────────────────────────────────┐
│ Eagerly loaded │
│ │
│ AppModule imports UserModule │
│ → UserModule bundled in initial bundle │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Lazily loaded │
│ │
│ { │
│ path: 'users', │
│ loadChildren: () => import(...) │
│ } │
│ │
│ → UserModule bundled separately │
│ → Loaded on navigation │
│ │
└──────────────────────────────────────────────┘
Visual: SharedModule Pattern
┌──────────────────────────────────────────────┐
│ SharedModule │
│ │
│ declarations: │
│ • ButtonComponent │
│ • CardComponent │
│ • HighlightDirective │
│ • TruncatePipe │
│ │
│ exports: │
│ • All the above │
│ • CommonModule │
│ │
└──────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ UserModule │ │ ProductModule│ │ OrderModule │
│ │ │ │ │ │
│ imports: │ │ imports: │ │ imports: │
│ SharedModule│ │ SharedModule│ │ SharedModule│
└─────────────┘ └─────────────┘ └─────────────┘
Visual: Bootstrap Comparison
┌──────────────────────────────────────────────┐
│ Module-based │
│ │
│ main.ts │
│ ─ platformBrowserDynamic() │
│ .bootstrapModule(AppModule); │
│ │
│ app.module.ts │
│ ─ @NgModule({ bootstrap: [AppComponent] }) │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Standalone │
│ │
│ main.ts │
│ ─ bootstrapApplication( │
│ AppComponent, │
│ appConfig │
│ ); │
│ │
│ app.config.ts │
│ ─ providers: [provideRouter(routes), ...] │
│ │
└──────────────────────────────────────────────┘
Visual: Module Dependencies
┌──────────────────────────────────────────────┐
│ AppModule │
│ ─ imports BrowserModule │
│ ─ imports AppRoutingModule │
│ │
└──────────────────────────────────────────────┘
│
│ imports
▼
┌──────────────────────────────────────────────┐
│ UserModule │
│ ─ imports CommonModule │
│ ─ imports SharedModule │
│ ─ imports UserRoutingModule │
│ │
└──────────────────────────────────────────────┘
│
│ imports
▼
┌──────────────────────────────────────────────┐
│ SharedModule │
│ ─ imports CommonModule │
│ ─ exports CommonModule │
│ ─ exports shared components │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ New component? │
│ │ │
│ └── standalone: true │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Lazy loading a feature? │
│ │ │
│ ├── Single component ──► loadComponent │
│ │ │
│ └── Multiple components ──► loadChildren│
│ (may use a module) │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Working on an existing module-based app? │
│ │ │
│ ├── Migrate over time ──► yes │
│ │ │
│ └── Keep modules ──► fine │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Service scope? │
│ │ │
│ ├── App-wide ──► providedIn: 'root' │
│ │ │
│ └── Module-scoped ──► providers array │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| NgModule | Class with @NgModule |
declarations | Components, directives, pipes owned |
imports | Modules and standalone components needed |
exports | What other modules can use |
providers | Services registered |
bootstrap | Root component (root only) |
| Root module | Bootstraps the app |
| Feature module | Organizes code by feature |
| Shared module | Bundles reusable pieces |
providedIn | Modern service registration |
| Standalone | Alternative without modules |
Key takeaways:
- An NgModule is a class with
@NgModulethat groups declarations, imports, exports, and providers declarationslists components, directives, and pipes the module owns — each belongs to exactly one moduleimportslists modules and standalone components the module needsexportscontrols what other modules can use — only exported items are visibleprovidersregisters services —providedIn: 'root'is the modern alternativebootstrapnames the root component — only in the root moduleBrowserModulegoes in the root module only;CommonModulein featuresSharedModulebundles reusable components and re-exportsCommonModule- Feature modules organize code by feature and enable lazy loading
providedIn: 'root'is the modern service pattern — tree-shakable, no module needed- Standalone components are the modern default since Angular 19 — they declare their own imports
- Migration is incremental — add
standalone: true, move imports, delete modules - Lazy loading still works with modules (
loadChildren) and standalone (loadComponent)
Remember: NgModules were Angular’s way to organize code, and they’re still in every existing app. They group related pieces, control visibility, and enable lazy loading. But standalone components replaced them for most new code — declaring dependencies directly is simpler and clearer. Know both: modules for the code you’ll encounter and the lazy-loading cases that still use them, standalone for everything new. The migration is incremental, and the destination is fewer files, clearer dependencies, and less ceremony.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!