← Back to Articles         
Migrate a Large NgModule Application Incrementally to Standalone

Migrate a Large NgModule Application Incrementally to Standalone

Published on 02 Oct 2026     13 min read Angular
Older Angular vs Modern Angular

1. How Do You Migrate a Large NgModule Application Incrementally to Standalone?

What is incremental migration?

Incremental migration means converting an existing NgModule-based Angular application to standalone APIs gradually, rather than rewriting the entire application at once.

For a large enterprise application, you may have:

AppModule
 ├── CoreModule
 ├── SharedModule
 ├── ProductsModule
 ├── OrdersModule
 ├── CustomersModule
 └── AdminModule

You do not need to convert everything simultaneously.

Angular supports interoperability between:

NgModule-based code
        ↕
Standalone code

Therefore, a safer migration can look like:

Existing NgModule Application
        ↓
Convert individual components
        ↓
Convert feature areas
        ↓
Convert routes
        ↓
Convert interceptors/guards/providers
        ↓
Bootstrap standalone
        ↓
Remove unnecessary NgModules

Why Migrate Incrementally?

A large enterprise Angular application may contain:

  • Hundreds of components
  • Shared libraries
  • Lazy-loaded modules
  • Route guards
  • HTTP interceptors
  • Custom directives
  • Custom pipes
  • Third-party NgModules
  • Complex routing
  • Authentication
  • State management

A complete rewrite creates unnecessary risk.

Incremental migration allows:

Convert
  ↓
Build
  ↓
Test
  ↓
Deploy
  ↓
Convert next area

This reduces the blast radius of each change.


Example Existing Application

Assume we start with:

src/app/

├── app.module.ts
├── app-routing.module.ts
│
├── core/
│   └── core.module.ts
│
├── shared/
│   └── shared.module.ts
│
├── products/
│   └── products.module.ts
│
└── admin/
    └── admin.module.ts

The initial startup is:

main.ts
   ↓
AppModule
   ↓
AppRoutingModule
   ↓
Feature Modules

Step 1 — Do Not Remove AppModule First

A common mistake is trying to immediately delete:

AppModule

You don't need to start there.

Keep the existing application bootstrapping:

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

Then migrate lower-level components/features gradually.

Think:

Leaves first
    ↓
Branches
    ↓
Root last

Step 2 — Convert a Simple Component

Suppose we have:

@Component({
  selector: 'app-product-card',
  templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
}

and it is declared in:

@NgModule({
  declarations: [
    ProductCardComponent
  ]
})
export class ProductsModule {
}

You can convert the component to standalone.

In modern Angular:

@Component({
  selector: 'app-product-card',

  imports: [
    CurrencyPipe,
    RouterLink
  ],

  templateUrl: './product-card.html'
})
export class ProductCard {
}

The component now explicitly owns its template dependencies.


Step 3 — Remove It From declarations

Previously:

@NgModule({
  declarations: [
    ProductCardComponent
  ]
})
export class ProductsModule {
}

Once converted appropriately, it is no longer an NgModule declaration.

Instead, an NgModule can consume a standalone component through imports:

@NgModule({

  imports: [
    CommonModule,
    ProductCard
  ]

})
export class ProductsModule {
}

This is one of the key mechanisms that makes incremental migration possible.

ProductsModule
      ↓
imports
      ↓
Standalone ProductCard

You don't have to convert ProductsModule immediately.


Step 4 — Convert Shared Components, Directives and Pipes

Suppose the old application has:

SharedModule
 ├── HeaderComponent
 ├── LoadingComponent
 ├── HighlightDirective
 └── ShortDatePipe

Traditionally:

@NgModule({

  declarations: [
    HeaderComponent,
    LoadingComponent,
    HighlightDirective,
    ShortDatePipe
  ],

  exports: [
    HeaderComponent,
    LoadingComponent,
    HighlightDirective,
    ShortDatePipe
  ]

})
export class SharedModule {
}

Gradually convert these into standalone artifacts.

Then consuming components can import only what they need:

@Component({

  imports: [
    Header,
    Loading,
    HighlightDirective,
    ShortDatePipe
  ]

})
export class Products {
}

This can eventually reduce or eliminate large catch-all SharedModules.


Step 5 — Convert Feature Routes

Suppose the old Products feature is lazy-loaded.

Old

{
  path: 'products',

  loadChildren: () =>
    import('./products/products.module')
      .then(m => m.ProductsModule)
}

The feature might contain:

ProductsModule
      ↓
ProductsRoutingModule
      ↓
ProductListComponent
ProductDetailsComponent
ProductCreateComponent

You can gradually move toward standalone route configuration.


products.routes.ts

import { Routes } from '@angular/router';

export const PRODUCT_ROUTES: Routes = [

  {
    path: '',

    loadComponent: () =>
      import('./product-list')
        .then(m => m.ProductList)
  },

  {
    path: 'create',

    loadComponent: () =>
      import('./product-create')
        .then(m => m.ProductCreate)
  },

  {
    path: ':id',

    loadComponent: () =>
      import('./product-details')
        .then(m => m.ProductDetails)
  }

];

Then the parent route can become:

{
  path: 'products',

  loadChildren: () =>
    import('./products/products.routes')
      .then(m => m.PRODUCT_ROUTES)
}

Now:

ProductsModule          ❌
ProductsRoutingModule   ❌

products.routes.ts      ✓
Standalone components   ✓

Step 6 — Migrate Route Guards

An older application may contain:

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivate(): boolean {

    if (this.authService.isLoggedIn()) {
      return true;
    }

    this.router.navigate(['/login']);

    return false;
  }
}

You can migrate it independently to:

export const authGuard: CanActivateFn =
  () => {

    const authService = inject(AuthService);

    const router = inject(Router);

    return authService.isLoggedIn()
      ? true
      : router.createUrlTree(['/login']);

  };

Then:

{
  path: 'admin',

  canActivate: [
    authGuard
  ],

  loadChildren: () =>
    import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES)
}

Step 7 — Migrate HTTP Configuration

An old application might have:

imports: [
  HttpClientModule
]

with:

providers: [

  {
    provide: HTTP_INTERCEPTORS,
    useClass: AuthInterceptor,
    multi: true
  }

]

During migration, HTTP configuration can eventually move toward:

provideHttpClient(
  withInterceptors([
    authInterceptor,
    errorInterceptor
  ])
)

You don't necessarily need to rewrite every HTTP service.

This service:

constructor(
  private http: HttpClient
) {}

can continue working.

The major change is the provider configuration, not the fundamental HttpClient API.


Step 8 — Move Global Providers to Application Configuration

Old:

@NgModule({

  providers: [
    SomeService
  ]

})
export class AppModule {
}

Modern application-level configuration can be:

export const appConfig: ApplicationConfig = {

  providers: [

    provideRouter(routes),

    provideHttpClient(),

    SomeService

  ]

};

Services using:

@Injectable({
  providedIn: 'root'
})

often need no explicit provider entry.


Step 9 — Convert the Root Component

Eventually the root component can become standalone.

For example:

@Component({

  selector: 'app-root',

  imports: [
    RouterOutlet
  ],

  template: `
    <router-outlet />
  `

})
export class App {
}

Step 10 — Replace AppModule Bootstrap

Old main.ts:

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

Finally migrate to:

bootstrapApplication(App, appConfig)
  .catch(err => console.error(err));

Now the root AppModule can be removed once its remaining responsibilities have been migrated.


Complete Migration Flow

A practical order is:

PHASE 1
Existing NgModule application
        ↓
Keep application working


PHASE 2
Convert simple components
directives and pipes
        ↓
Standalone


PHASE 3
Convert feature areas
        ↓
Standalone components


PHASE 4
Convert feature routing
        ↓
Standalone Routes


PHASE 5
Modernize guards/interceptors
        ↓
Functional APIs


PHASE 6
Move global providers
        ↓
app.config.ts


PHASE 7
Convert root component
        ↓
Standalone App


PHASE 8
bootstrapApplication()
        ↓
Remove AppModule

The exact order can vary according to the application's dependency graph and test coverage.


Important: NgModules and Standalone Can Coexist

This is probably the most important migration point.

You can temporarily have:

Application

├── Old AdminModule
├── Old ReportsModule
├── Standalone Products
├── Standalone Orders
└── Standalone Shared Components

There is no requirement to migrate everything in one release.


2. What Knowledge From Older Angular Applications Is Still Important When Maintaining Enterprise Applications?

This is extremely important for interviews and real enterprise maintenance.

Knowing only modern standalone Angular is often insufficient when maintaining mature applications.

An enterprise codebase may have been started years ago and contain multiple Angular generations.

For example:

Old code
   +
Modernized code
   +
New standalone code

Therefore you should understand both.


1. NgModule

You should understand:

@NgModule({
  declarations: [],
  imports: [],
  exports: [],
  providers: [],
  bootstrap: []
})

Especially:

declarations
imports
exports
providers
bootstrap

because many enterprise applications still contain feature/shared modules.


2. AppModule

You should recognize:

@NgModule({

  imports: [
    BrowserModule
  ],

  bootstrap: [
    AppComponent
  ]

})
export class AppModule {
}

and understand startup:

main.ts
   ↓
AppModule
   ↓
AppComponent

even if new applications use:

bootstrapApplication()

3. AppRoutingModule

You should understand traditional routing:

@NgModule({

  imports: [
    RouterModule.forRoot(routes)
  ],

  exports: [
    RouterModule
  ]

})
export class AppRoutingModule {
}

because existing applications may not use:

provideRouter()

4. RouterModule.forRoot() and forChild()

You should understand:

RouterModule.forRoot(routes)

for root routing and:

RouterModule.forChild(routes)

for feature routing.

Typical enterprise structure:

AppRoutingModule
      ↓
forRoot()


ProductsRoutingModule
      ↓
forChild()


AdminRoutingModule
      ↓
forChild()

5. Feature Modules

Understand structures such as:

ProductsModule
OrdersModule
AdminModule
ReportsModule

and why applications were organized this way.

For example:

@NgModule({

  declarations: [
    ProductListComponent,
    ProductDetailsComponent
  ],

  imports: [
    CommonModule,
    ProductsRoutingModule
  ]

})
export class ProductsModule {
}

6. Shared Modules

Older applications frequently contain:

SharedModule

with reusable:

Components
Directives
Pipes
Angular modules

For example:

@NgModule({

  declarations: [
    LoadingComponent,
    HighlightDirective
  ],

  imports: [
    CommonModule
  ],

  exports: [
    LoadingComponent,
    HighlightDirective,
    CommonModule
  ]

})
export class SharedModule {
}

Even if you prefer direct standalone imports today, you must understand this pattern to maintain older code.


7. HttpClientModule

You should recognize:

imports: [
  HttpClientModule
]

and know that modern provider-based configuration commonly uses:

provideHttpClient()

8. Class-Based Interceptors

Enterprise applications may contain:

@Injectable()
export class AuthInterceptor
  implements HttpInterceptor {

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ) {

    return next.handle(req);

  }

}

registered through:

{
  provide: HTTP_INTERCEPTORS,
  useClass: AuthInterceptor,
  multi: true
}

You should understand this even when writing new functional interceptors.


9. Class-Based Route Guards

You may encounter:

@Injectable({
  providedIn: 'root'
})
export class AuthGuard
  implements CanActivate {

  canActivate() {
    return true;
  }
}

while newer code may use:

export const authGuard: CanActivateFn =
  () => true;

Both are important for maintenance knowledge.


10. Constructor Dependency Injection

Older Angular code heavily uses:

constructor(
  private productService: ProductService,
  private router: Router,
  private http: HttpClient
) {}

Modern code may use:

private productService =
  inject(ProductService);

private router =
  inject(Router);

You should understand both styles.


11. Lazy-Loaded Feature Modules

You should recognize:

loadChildren: () =>
  import('./admin/admin.module')
    .then(m => m.AdminModule)

and understand the modern alternative:

loadChildren: () =>
  import('./admin/admin.routes')
    .then(m => m.ADMIN_ROUTES)

or:

loadComponent: () =>
  import('./products')
    .then(m => m.Products)

12. RxJS Is Still Extremely Important

Standalone Angular and Signals do not make RxJS knowledge obsolete.

Enterprise Angular applications commonly rely heavily on:

Observable
Subject
BehaviorSubject
ReplaySubject

and operators such as:

map
filter
tap
switchMap
mergeMap
concatMap
exhaustMap
catchError
debounceTime
distinctUntilChanged
combineLatest
forkJoin
shareReplay

You should be comfortable reading code like:

this.searchControl.valueChanges
  .pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(search =>
      this.productService.search(search)
    ),
    catchError(error => {
      console.error(error);
      return of([]);
    })
  );

Signals complement RxJS; they do not eliminate the need to understand RxJS.


13. AsyncPipe

Enterprise applications frequently use:

{{ user$ | async }}

or:

<div *ngFor="let product of products$ | async">

Even if newer code uses Signals:

@for (product of products(); track product.id) {

you need to understand both.


14. Older Template Syntax

You should understand:

*ngIf
*ngFor
ngSwitch

even though newer Angular supports control-flow syntax:

@if
@for
@switch

For example:

Older

<div *ngIf="isLoggedIn">
  Welcome
</div>

Modern

@if (isLoggedIn) {
  <div>Welcome</div>
}

Enterprise code will often contain both styles.


15. Reactive Forms and Template-Driven Forms

These concepts remain highly relevant.

You should understand:

FormControl
FormGroup
FormArray
FormBuilder
Validators
ControlValueAccessor

Standalone Angular changes how form dependencies may be imported, but the fundamental forms concepts remain.


16. Lifecycle Hooks

Still understand:

ngOnChanges
ngOnInit
ngDoCheck
ngAfterContentInit
ngAfterContentChecked
ngAfterViewInit
ngAfterViewChecked
ngOnDestroy

Standalone components do not eliminate component lifecycle concepts.


17. Dependency Injection

The fundamental DI concepts remain critical:

Provider
Token
Injector
Service lifetime/scope
Hierarchical injectors
InjectionToken

The syntax may change:

constructor(private service: MyService) {}

versus:

private service = inject(MyService);

but the underlying Angular DI model is still important.


18. Change Detection

You should still understand:

Default change detection
OnPush
Zone.js
Zoneless Angular
Signals

because enterprise applications may contain older Zone.js-centric patterns alongside newer signal-based code.


Old Knowledge vs Modern Equivalent

Older Angular Modern Angular
AppModule Standalone bootstrap/configuration
bootstrapModule() bootstrapApplication()
AppRoutingModule app.routes.ts + provideRouter()
declarations Standalone component imports
HttpClientModule provideHttpClient()
HTTP_INTERCEPTORS withInterceptors() for functional interceptors
Class interceptor Functional interceptor
Class guard Functional guard
Constructor DI Constructor DI or inject()
Lazy feature NgModule Standalone routes/components
RouterModule.forRoot() provideRouter()
*ngIf @if
*ngFor @for
BehaviorSubject state Signals may simplify some state
AsyncPipe Still useful; Signals also integrate directly with templates

What Should an Enterprise Angular Developer Know?

For interview preparation, I would divide the knowledge into three levels.

Level 1 — Must understand older architecture

NgModule
AppModule
FeatureModule
SharedModule
AppRoutingModule
declarations / imports / exports / providers
RouterModule.forRoot()
RouterModule.forChild()
HttpClientModule
HTTP_INTERCEPTORS
Class Guards
Lazy-loaded NgModules

Level 2 — Must understand modern Angular

Standalone Components
bootstrapApplication()
app.config.ts
provideRouter()
provideHttpClient()
Functional Interceptors
Functional Guards
loadComponent()
Standalone lazy routes
Signals
Modern template control flow
inject()

Level 3 — Must understand concepts that remain fundamental

Components
Templates
Dependency Injection
Routing
Reactive Forms
HttpClient
RxJS
Observables
Change Detection
Lifecycle Hooks
Authentication
Authorization
Lazy Loading
Testing
State Management

Key Points

  • A large NgModule application does not need to be rewritten all at once.
  • Standalone and NgModule-based code can coexist during migration.
  • Start with isolated components/directives/pipes where practical.
  • Migrate feature areas gradually.
  • Lazy-loaded NgModules can gradually become standalone route groups.
  • Class guards/interceptors can be modernized independently where appropriate.
  • Move application-level providers toward standalone configuration.
  • Convert the root/bootstrap layer after lower-level dependencies are ready.
  • Test after each migration step.
  • Older Angular knowledge remains important because enterprise applications frequently contain legacy and modern patterns together.
  • RxJS remains important even in Signal-heavy modern Angular applications.
  • Standalone Angular changes application organization and configuration; it does not invalidate Angular fundamentals.

Interview Questions and Answers

1. Can an NgModule application be migrated incrementally to standalone?

Yes. Angular supports interoperability between NgModule-based and standalone code, allowing components and feature areas to be migrated gradually.


2. Do we need to remove AppModule first?

No. It is often safer to keep the existing root bootstrap initially, migrate lower-level features, and move to bootstrapApplication() later.


3. Can an NgModule use a standalone component?

Yes. This interoperability is one of the mechanisms that allows gradual migration.

Conceptually:

@NgModule({
  imports: [
    ProductCard
  ]
})
export class ProductsModule {}

4. Should a large Angular application be rewritten completely just to use standalone APIs?

Usually an incremental migration is lower risk. Existing stable functionality can continue operating while individual areas are modernized and tested.


5. What should be migrated first?

There is no universal order, but relatively isolated components, directives and pipes are often good starting points. Feature routes and global bootstrap configuration can follow as dependencies permit.


6. What happens to lazy-loaded feature modules?

A route such as:

loadChildren: () =>
  import('./products/products.module')
    .then(m => m.ProductsModule)

can eventually become standalone routes:

loadChildren: () =>
  import('./products/products.routes')
    .then(m => m.PRODUCT_ROUTES)

7. Is old NgModule knowledge still important?

Yes. Many enterprise applications still contain AppModule, feature modules, shared modules, module-based routing and other older patterns.


8. Should I still learn RouterModule.forRoot() and forChild()?

Yes, particularly for maintenance work.

Older applications frequently use:

RouterModule.forRoot(routes)

and:

RouterModule.forChild(routes)

while modern standalone applications commonly use:

provideRouter(routes)

9. Should I still learn HTTP_INTERCEPTORS?

Yes.

Older enterprise applications commonly register class-based interceptors using:

{
  provide: HTTP_INTERCEPTORS,
  useClass: AuthInterceptor,
  multi: true
}

New code commonly favors functional interceptors with:

provideHttpClient(
  withInterceptors([
    authInterceptor
  ])
)

10. Is RxJS still important after Signals?

Yes.

Signals are very useful for reactive state, while RxJS remains highly valuable for asynchronous streams, HTTP workflows, debouncing, cancellation, WebSockets and stream composition.


11. Should I learn class-based guards?

Yes, if you expect to maintain existing Angular applications.

You should recognize:

class AuthGuard implements CanActivate

as well as modern:

const authGuard: CanActivateFn

12. What is the safest migration strategy for a large Angular application?

A concise interview answer is:

Migrate incrementally. Keep NgModule and standalone code interoperating, convert isolated components and feature areas first, migrate routing and providers progressively, verify each stage with tests, and replace the root NgModule bootstrap only when its responsibilities have been moved.


Interview Quick Revision

LARGE ENTERPRISE MIGRATION
==========================

Existing NgModule App
        ↓
Standalone components
        ↓
Standalone feature areas
        ↓
Standalone routes
        ↓
Modern guards/interceptors
        ↓
Application providers
        ↓
Root component
        ↓
bootstrapApplication()
        ↓
Remove unnecessary NgModules

And for enterprise maintenance:

YOU NEED BOTH
=============

OLDER ANGULAR
NgModule
AppModule
SharedModule
FeatureModule
HTTP_INTERCEPTORS
Class Guards
Lazy NgModules
RxJS
*ngIf / *ngFor

        +

MODERN ANGULAR
Standalone
bootstrapApplication()
provideRouter()
provideHttpClient()
Functional Interceptors
Functional Guards
Signals
@if / @for
loadComponent()

One-Line Interview Answer

A large NgModule application should generally be migrated incrementally because Angular allows NgModule and standalone code to coexist. Enterprise developers should understand both older patterns—such as NgModules, module-based routing, class guards/interceptors and RxJS—and modern standalone APIs, because mature applications often contain both generations of Angular code.

← Back to Articles