| |

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:

PropertyPurpose
declarationsComponents, directives, pipes owned by this module
importsOther modules this module needs
exportsWhat this module exposes to other modules
providersServices this module provides
bootstrapRoot 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:

ModuleProvides
BrowserModuleCore browser services (root only)
CommonModule*ngIf, *ngFor, built-in pipes
FormsModulengModel, template-driven forms
ReactiveFormsModuleForm groups, controls
HttpClientModuleHTTP client (legacy)
RouterModuleRouting 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 CommonModule is separate from BrowserModule: BrowserModule includes everything CommonModule does, plus browser-specific setup. Feature modules don’t need the browser setup — that’s done once in the root module. So CommonModule is 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 CommonModule is common: Every feature module needs *ngIf and *ngFor. Without re-exporting, each feature would import CommonModule itself. The SharedModule pattern 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:

ValueScope
'root'App-wide singleton
'platform'Across multiple apps on the page
'any'New instance per module that uses it
SomeModuleScoped 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 declarations and bootstrap
  • 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. BrowserModule can only be imported once — in the root module. That’s why feature modules use CommonModule instead. 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 loadComponent for 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 SharedModule re-exports CommonModule: The shared components need *ngIf and *ngFor from CommonModule. By re-exporting it, feature modules get both the components and CommonModule in one import. The feature modules don’t need to import CommonModule separately.


NgModule vs standalone

The comparison between the two approaches.

AspectNgModuleStandalone
Component declarationIn declarationsIn imports of the component
DependenciesModule importsComponent imports
VisibilityModule exportsExplicit export
Lazy loadingloadChildrenloadComponent or loadChildren
BootstrappingbootstrapModulebootstrapApplication
Default sincePre-19Angular 19+
Removed?NoN/A
RecommendedLegacyModern

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:

  1. Add standalone: true to the component
  2. Move the module’s imports into the component’s imports
  3. Remove the component from declarations
  4. 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: true on each component
  • imports on each component, not the module
  • Service uses providedIn: 'root' instead of module providers

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

PropertyPurpose
declarationsComponents, directives, pipes
importsOther modules to use
exportsWhat other modules can use
providersServices to register
bootstrapRoot component (root only)

What Goes Where

ItemWhere
Componentdeclarations
Directivedeclarations
Pipedeclarations
Moduleimports
Standalone componentimports
Serviceproviders or providedIn: 'root'
Injection tokenproviders

Common Modules

ModuleProvidesFor
BrowserModuleBrowser setupRoot only
CommonModule*ngIf, *ngFor, pipesFeatures
FormsModulengModelTemplate forms
ReactiveFormsModuleFormGroup etc.Reactive forms
HttpClientModuleHTTP clientLegacy HTTP
RouterModuleRoutingRoot + features

providedIn Options

ValueScope
'root'App-wide singleton
'platform'Across apps
'any'Per module
SomeModuleModule-scoped
(no providedIn)Must be in providers

Feature Module Structure

FilePurpose
feature.module.tsModule with declarations + imports
feature-routing.module.tsRoutes for the feature
ComponentsIn declarations
ServiceprovidedIn: 'root' or in providers

forRoot vs forChild

FormUse
RouterModule.forRoot(routes)Root module only
RouterModule.forChild(routes)Feature modules

Lazy Loading

ApproachSyntax
ModuleloadChildren: () => import('./m').then(m => m.Module)
StandaloneloadComponent: () => import('./c').then(c => c.Component)

NgModule vs Standalone

AspectNgModuleStandalone
Component declarationdeclarationsimports of component
Default sincePre-19Angular 19
RecommendedLegacyModern
Files per component2+1
RemovedNoN/A

Migration Steps

StepAction
1Add standalone: true
2Move module imports to component
3Remove from declarations
4Delete module if unused
5Use bootstrapApplication

SharedModule Contents

IncludeExclude
Reusable componentsServices
Reusable directivesFeature components
Reusable pipesSingle-use components
CommonModule re-exportApp-wide modules

Common Errors

ErrorCauseFix
Component declared twiceIn two modulesDeclare once
Not declared in any moduleMissing declarationAdd to a module or make standalone
BrowserModule twiceImported in featureUse CommonModule
CommonModule missingNot importedAdd to imports
Module imports not availableNot exportedExport the module or item

Bootstrap Comparison

AspectModuleStandalone
Entry pointmain.tsmain.ts
FunctionbootstrapModule(AppModule)bootstrapApplication(AppComponent)
ProvidersIn @NgModuleIn second argument

Best Practices Summary

RuleReason
Standalone for new codeModern default
Modules for lazy loadingStill works
providedIn: 'root' for servicesTree-shakable
SharedModule for reusable UIReduce boilerplate
CommonModule in featuresNot BrowserModule
Feature modules for featuresBoundary
Root module only for bootstrapBrowserModule 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

PitfallProblemSolution
Component in two modulesCompile errorDeclare once
Not declared anywhereTemplate errorAdd to module or make standalone
BrowserModule in featureRuntime errorUse CommonModule
Missing CommonModule*ngIf not foundAdd to imports
Service in declarationsCompile errorMove to providers
forRoot in featureMultiple router instancesUse forChild
SharedModule too bigImports bloatInclude only shared pieces
Lazy module imports featureLoads unnecessarilySplit modules
Standalone in declarationsCompile errorImport it instead
Migration half-doneMixed patternsMigrate 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

ConceptMeaning
NgModuleClass with @NgModule
declarationsComponents, directives, pipes owned
importsModules and standalone components needed
exportsWhat other modules can use
providersServices registered
bootstrapRoot component (root only)
Root moduleBootstraps the app
Feature moduleOrganizes code by feature
Shared moduleBundles reusable pieces
providedInModern service registration
StandaloneAlternative without modules

Key takeaways:

  • An NgModule is a class with @NgModule that groups declarations, imports, exports, and providers
  • declarations lists components, directives, and pipes the module owns — each belongs to exactly one module
  • imports lists modules and standalone components the module needs
  • exports controls what other modules can use — only exported items are visible
  • providers registers services — providedIn: 'root' is the modern alternative
  • bootstrap names the root component — only in the root module
  • BrowserModule goes in the root module only; CommonModule in features
  • SharedModule bundles reusable components and re-exports CommonModule
  • 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!