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.