With standalone, AppModule is no longer the central “assembly point”. Template scope moves from NgModule declarations/imports to each standalone component’s imports, and app-wide configuration moves to bootstrapApplication(...) (or an ApplicationConfig) and route-level providers. AppModule often disappears in greenfield apps, but NgModules still exist for legacy code and module-shaped libraries. The trade-off is simpler bootstrapping versus managing provider scope per component; test DI boundaries when migrating.
AppModule in the standalone era: what moved to components, bootstrapApplication, and route providers?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
Standalone changes Angular’s composition model:
Before: AppModule declared what exists and what templates can use.
Now: standalone components declare their own template dependencies (imports), and app-wide wiring happens in bootstrapApplication (or ApplicationConfig) + route providers.
AppModule responsibility (NgModule-era) | Standalone replacement (where it moved) | Concrete example |
|---|---|---|
Bootstrapping | main.ts uses |
|
Template dependency scope (CommonModule/Router/Material/etc.) | Standalone component |
|
Global providers via module imports | Provider functions + environment providers |
|
Feature modules for routing + lazy loading | Standalone routes + |
|
Legacy declarables packaging | Still NgModules (compat layer) | Standalone components import the legacy NgModule that exports them |
// NgModule-era (classic)
// main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule).catch(console.error);
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, RouterModule.forRoot([])],
bootstrap: [AppComponent]
})
export class AppModule {}
// Standalone-era (modern)
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([
// (req, next) => next(req)
])
),
provideAnimations()
]
};
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { appConfig } from './app.config';
bootstrapApplication(AppComponent, appConfig).catch(console.error);
// app.component.ts (standalone root)
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `<router-outlet></router-outlet>`
})
export class AppComponent {}
Interview hotspot: “Where do global things go now?” | Standalone answer |
|---|---|
RouterModule.forRoot(...) |
|
HttpClientModule |
|
BrowserAnimationsModule |
|
Feature-scoped providers | Route-level |
Module-only libraries (forRoot/NgModules) | Use |
// Route-level providers (feature scoping without a feature module)
import { Routes } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.page').then(m => m.AdminPage),
providers: [
// providers here are scoped to the admin route subtree
provideHttpClient()
]
}
];
Common standalone migration pitfall | What breaks | Fix |
|---|---|---|
Forgetting template imports (NgIf/NgFor/AsyncPipe/etc.) | Template compile errors: “Can’t bind to ...” / unknown directive/pipe | Import the standalone directive/pipe (e.g. |
Mixing module config patterns blindly | Duplicate providers / unexpected multiple instances | Prefer provider functions at bootstrap/route; use |
Assuming NgModules are “gone” | Legacy libs/declarables can’t be imported directly | Keep NgModules as a compatibility/container layer where needed |
Do you still need AppModule? | Answer |
|---|---|
Greenfield standalone app | Usually no — AppModule can disappear entirely. |
Incremental migration from a large NgModule app | Often yes (temporarily) — convert routes/components gradually while keeping AppModule as glue. |
Library ecosystem is NgModule-heavy (forRoot/exported modules) | You might still avoid AppModule, but you’ll rely on |
Practical scenario
Migrating a legacy Angular app to standalone components, you move imports and providers from AppModule into each feature component.
Common pitfalls
- Forgetting to add required imports to a standalone component.
- Provider scope changes causing unexpected singleton behavior.
- Mixing module-based libraries without clear boundaries.
Standalone simplifies bootstrapping but shifts responsibility to each component. Test DI scopes and route-level providers after migration.
Standalone doesn’t delete NgModules, but it removes the need for an AppModule as the app’s “wiring center”. Template scope moves to standalone component imports. App-wide wiring moves to bootstrapApplication (or ApplicationConfig) and can be further scoped via route-level providers. AppModule becomes optional: common to remove in new apps, common to keep temporarily during migrations.