Tutorials Logic, IN info@tutorialslogic.com

Angular NgModules, Feature Modules and Standalone Interop

NgModule Purpose

Angular recommends standalone components for new code. NgModules remain essential when maintaining module-based applications or integrating libraries that expose module APIs. This lesson covers their compilation scope, provider behavior, feature boundaries, lazy loading, standalone interoperability, and safe migration.

An NgModule defines a compilation scope for non-standalone components, directives, and pipes and can also contribute providers to an injector. NgModules remain supported for existing applications and libraries, but standalone components are the default for new Angular code.

declarations owns non-standalone declarables. imports makes exported declarations from other modules or standalone declarables available to this module. exports publishes selected declarations and imports to consumers.

providers registers dependencies in the module injector. bootstrap names root components only for an application started through bootstrapModule. The old entryComponents field is unnecessary with the Ivy compiler and should not be added to modern modules.

NgModule Bootstrap Compatibility

NgModule Bootstrap Compatibility
import { Component, NgModule } from '@angular/core';
import { BrowserModule, platformBrowser } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  standalone: false,
  template: `<h1>Module-based application</h1>`
})
export class AppComponent {}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

platformBrowser()
  .bootstrapModule(AppModule)
  .catch(error => console.error(error));

Current Angular declarables are standalone by default, so a component, directive, or pipe placed in declarations must explicitly set standalone: false. BrowserModule belongs only in the root browser NgModule.

Standalone and NgModule Apps

A standalone component declares template dependencies in its own imports and an application starts with bootstrapApplication plus ApplicationConfig. This makes ownership visible at the component and application boundaries.

Standalone and NgModule code can interoperate. A standalone component can import an NgModule, and an NgModule can import and export a standalone declarable. Migrate feature by feature instead of rewriting an application at once.

Do not use an NgModule merely as a folder or organization mechanism. Keep one when a library exposes a module API, an existing feature already has a stable module boundary, or provider configuration relies on that integration.

Standalone Bootstrap

Standalone Bootstrap
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));

Standalone Application Providers

Standalone Application Providers
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient()
  ]
};

Provider functions replace many root NgModule imports in standalone applications. Keep deprecated animation providers out of new code; current Angular animations use animate.enter and animate.leave.

Standalone and NgModule Apps Usage

Standalone and NgModule Apps Usage
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: `<router-outlet />`
})
export class AppComponent { }

NgModule Metadata

NgModule metadata defines a compilation scope for non-standalone declarables and can add providers to an injector. Each field has a different ownership rule; confusing declarations with imports is the most common module error.

Field Responsibility
declarations Owns non-standalone components, directives, and pipes. A declarable belongs to exactly one NgModule.
imports Makes exported NgModule declarations and standalone declarables available in this module's templates.
exports Publishes selected declarations or imported dependencies to modules that import this NgModule.
providers Registers dependencies in the module injector; prefer current provide... APIs for new application-wide configuration.
bootstrap Names root components only for an application started through an NgModule.
schemas Changes template validation for specific custom-element integration cases; do not use it to hide missing imports.
  • Do not put services, ordinary classes, or standalone declarables in declarations.
  • Do not add entryComponents; Ivy creates component factories without that metadata.
  • Avoid a large SharedModule that silently exports unrelated dependencies to every feature.

Root, Feature and Shared Modules

A module name describes an architectural role, not a different Angular type. Root, feature, shared, routing, and core modules all use @NgModule; the difference is what they own, export, configure, and where they are imported.

Keep a feature module aligned with one domain capability. It owns its non-standalone pages, directives, and pipes, imports their direct template dependencies, and exposes only the declarations another feature must use. A shared module should contain reusable presentation pieces, not application state or unrelated dependencies.

Module role Owns Important boundary
Root AppModule Root component, BrowserModule, bootstrap and compatibility root providers Import BrowserModule once and bootstrap only when the app uses bootstrapModule.
Feature module One domain area such as orders, billing or administration Import direct dependencies; avoid reaching through another feature module.
Shared module Reusable non-standalone UI, directives and pipes Keep it small and normally provider-free so importing it cannot create hidden state.
Routing module RouterModule.forChild routes for one module-based feature Exports RouterModule only when declared templates need router directives.
Core module convention Older application-wide infrastructure Prefer providedIn root or ApplicationConfig in current code; never import a provider-heavy core module repeatedly.
  • Do not create a module merely because a directory exists. Create a module only when compatibility code needs a compilation, provider, export, or routing boundary.
  • A feature module can import standalone components, directives, and pipes directly in its imports array.
  • Export the smallest public surface. Internal page components do not need to be exported just because they are declared.

Focused Feature Module

Focused Feature Module
import { Component, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';

@Component({
  selector: 'app-orders-page',
  standalone: false,
  template: `<h2>Orders</h2>`
})
export class OrdersPage {}

const routes: Routes = [
  { path: '', component: OrdersPage }
];

@NgModule({
  declarations: [OrdersPage],
  imports: [CommonModule, RouterModule.forChild(routes)]
})
export class OrdersModule {}

The feature owns one page and its child route. It imports CommonModule instead of BrowserModule and does not export a route-only page that no other template consumes.

Framework Module Imports

A TypeScript import makes a class name available in a file. The @NgModule imports array changes the Angular compilation or provider scope. You usually need both: import the symbol from its package, then place the NgModule or standalone dependency in metadata.

Import only what declared templates need. Built-in control flow such as @if and @for does not require CommonModule, but compatibility directives and pipes such as NgClass, AsyncPipe, DatePipe, and JsonPipe do.

Dependency Where it belongs What it supplies
BrowserModule Root browser NgModule only Browser platform services and a re-export of CommonModule.
CommonModule Feature and shared NgModules as needed Common directives and pipes without reinstalling browser platform providers.
FormsModule Module whose templates use template-driven forms ngModel and template-driven form directives.
ReactiveFormsModule Module whose templates use reactive forms formGroup, formControl and reactive forms directives.
RouterModule.forRoot(routes) Root module once Router services plus the root route configuration.
RouterModule.forChild(routes) Feature routing module Additional routes without reinstalling the Router service.
  • An unknown element or pipe often means the declaring module lacks the dependency in imports, not that the dependency is missing from package.json.
  • Do not export every imported module from a SharedModule. Re-export only the dependencies consumers intentionally receive.
  • For new standalone applications, prefer provideRouter and provideHttpClient over adding compatibility root modules.

Providers and forRoot Patterns

NgModule imports combine provider records as Angular builds an injector. Providers from eagerly imported modules normally join the application injector. A lazy-loaded NgModule receives a child environment injector, so a token provided there can resolve to a feature-specific instance.

The forRoot convention separates one-time application configuration from reusable declarations. Import Module.forRoot(config) once at the root and import the plain module, or use its forChild API, elsewhere. For new libraries, a typed provideFeature(config) function is usually clearer and works naturally with standalone bootstrapping.

  • Prefer @Injectable({ providedIn: 'root' }) for a tree-shakable application singleton unless a narrower lifetime is required.
  • Do not place a stateful singleton in a SharedModule that many eager and lazy features import without first deciding its injector lifetime.
  • When the same token has multiple non-multi providers in one injector, the later effective provider can replace the earlier one. Treat import order fixes as a warning to clarify ownership.
  • Use multi: true only for intentional contribution lists such as interceptors or plugin tokens; every contribution must use the same multi-provider contract.

Typed forRoot Configuration

Typed forRoot Configuration
import {
  InjectionToken,
  ModuleWithProviders,
  NgModule
} from '@angular/core';

export const API_BASE_URL =
  new InjectionToken<string>('API_BASE_URL');

@NgModule()
export class ApiClientModule {
  static forRoot(baseUrl: string):
      ModuleWithProviders<ApiClientModule> {
    return {
      ngModule: ApiClientModule,
      providers: [
        { provide: API_BASE_URL, useValue: baseUrl }
      ]
    };
  }
}

@NgModule({
  imports: [ApiClientModule.forRoot('/api')]
})
export class AppModule {}

ModuleWithProviders keeps the returned NgModule type explicit. Configure the provider once at the root; feature modules that need declarations from the package import its non-root API.

Lazy-Loaded NgModules

A module is not lazy merely because it is named FeatureModule. The router creates a separate chunk only when loadChildren reaches the module through a dynamic import and no eager import pulls that feature into the initial graph.

The lazy module configures its internal routes with RouterModule.forChild. Do not also import that feature module into AppModule. The router creates the module and its child injector when navigation first matches the lazy route.

  • Keep the root route path in the parent configuration and use an empty path inside the lazy module for its landing page.
  • Inspect the production build output or browser network panel to prove the feature is emitted and requested as a separate chunk.
  • Providers declared by the lazy module are scoped below its lazy injector unless another provider higher in the hierarchy wins lookup first.
  • Current standalone code usually lazy-loads a component or route array. Keep lazy NgModules for existing module-owned declarables or library boundaries that require them.

Lazy Feature Module Route

Lazy Feature Module Route
import { Routes } from '@angular/router';

export const appRoutes: Routes = [
  {
    path: 'orders',
    loadChildren: () =>
      import('./orders/orders.module')
        .then(module => module.OrdersModule)
  }
];

// OrdersModule owns an empty-path child route:
// RouterModule.forChild([
//   { path: '', component: OrdersPage }
// ])

The dynamic import creates the lazy boundary. OrdersModule must not also appear in an eager imports array, or the intended bundle split is lost.

NgModule Error Diagnosis

Read the first Angular compiler or injector diagnostic and classify it as ownership, compilation scope, export surface, provider scope, or routing. This is faster than adding modules to SharedModule until the error disappears.

Symptom Cause Smallest correction
Standalone declarable appears in declarations Current declarables default to standalone. Put it in imports, or deliberately mark compatibility code standalone: false.
Declared by more than one NgModule Two modules claim the same non-standalone declarable. Choose one owner, export from that owner, and import the owner elsewhere.
Unknown element, property or pipe The declaring module cannot see the dependency. Import the standalone dependency or the NgModule that exports it into the correct module.
Component is not visible to a consumer The owner declared it but did not export it. Export that public declarable from its owner; do not redeclare it.
BrowserModule already loaded A feature or lazy module imported BrowserModule again. Keep BrowserModule in the root and use CommonModule in features.
Unexpected service instance A provider was repeated or placed in a lazy child injector. Inspect the provider owner and injector hierarchy; move the provider to the intended root, route, or component boundary.
Lazy feature is in the initial bundle The feature module is also imported eagerly. Remove the eager import and retain only the loadChildren dynamic import.
  • Check the component standalone flag before deciding between declarations and imports.
  • Trace selector ownership: declarable, owner NgModule, exports, then consuming NgModule imports.
  • Trace provider ownership separately; template visibility and dependency injection are different scopes.
  • Rebuild after the minimal metadata change and verify the affected template, route, and service identity.

Standalone Migration

Migrate ownership before deleting a module. A component becomes standalone, imports its template dependencies directly, and moves application-wide provider configuration to bootstrap or route providers.

Angular provides an interactive standalone migration schematic. Run its phases in order, inspect every change, and commit between phases: convert declarations, switch bootstrap, then remove unnecessary NgModules. Manual cleanup is still required for application-specific providers and module APIs.

NgModule responsibility Standalone destination
declarations The component, directive, or pipe becomes standalone and owns its imports.
template imports The consuming standalone component imports each dependency directly.
root providers ApplicationConfig or providers passed to bootstrapApplication.
feature providers Route providers or the component provider boundary that should own the lifetime.
lazy feature module A lazy route using loadComponent or loadChildren with route arrays.
  • Choose one feature boundary and identify its declarations, imported template dependencies, providers, and routes.
  • Convert leaf components first so callers can import them from either standalone or NgModule code.
  • Replace RouterModule.forRoot, HttpClientModule, and similar root configuration with current provider APIs where supported.
  • Keep importProvidersFrom as a bridge for a library that exposes only NgModule configuration.
  • Run component tests and navigate every migrated route before removing the old module.

Standalone Migration Workflow

Standalone Migration Workflow
# Run once for each interactive migration phase
ng generate @angular/core:standalone

# Verify after every phase before continuing
ng test
ng build

Use a clean branch and review the schematic diff after each run. The migration can preserve NgModules that still bootstrap components or contain code it cannot safely remove.

Confirm the page outcome

Module Decisions

7 checks
  • I can explain why current Angular code normally starts standalone and when an NgModule remains useful.
  • I can place declarables, template dependencies, exports, providers, and bootstrap components in the correct metadata fields.
  • I can identify duplicate declaration, missing export, missing import, and accidentally shadowed provider failures.
  • I can migrate a feature incrementally without duplicating ownership or breaking lazy routes.
  • I avoid creating NgModules only to group files or conceal broad shared dependencies.
  • I can distinguish BrowserModule, CommonModule, forRoot, forChild, eager providers, and lazy child injectors.
  • I can prove whether a feature NgModule is genuinely lazy by inspecting its route configuration and production chunks.

Module Decisions Questions

A non-standalone component gets one owner. Declare it in the feature or shared module responsible for it, export it there, and import that module elsewhere. If the component is standalone, it belongs in imports, not declarations.

Move application-wide providers into ApplicationConfig or the providers passed to bootstrapApplication. Router configuration becomes provideRouter(routes), HTTP configuration uses provideHttpClient(), and other libraries usually expose a provide... function or an importProvidersFrom() bridge. Standalone and NgModule code interoperate, so migrate one route or feature at a time instead of treating the change as all-or-nothing.

BrowserModule and application-wide provider configuration belong at the root. A feature or lazy NgModule imports CommonModule plus only the forms, router, or UI dependencies its declarations use. In module APIs, forRoot() conventionally installs root providers once, while forChild() contributes feature configuration without reinstalling them; modern libraries may expose provide... functions instead.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.