Since Angular 19, every component, directive and pipe is standalone by default — you no longer write standalone: true and you no longer need an NgModule. Each component declares its own dependencies in imports, and the application starts with a function, not a module.
How the application starts
Three files: main.ts starts the application, app.config.ts holds the global providers, and app.ts is the root component. To build a small app on top of them, follow Your first app, step by step.
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));- v21: new apps are zoneless — no
zone.jsin the bundle, no special provider. - v21:
HttpClientis available in root withoutprovideHttpClient(). You call it only to configure (interceptors, XHR). - v22: components are
OnPushby default. See Change detection. - v22: TypeScript 6 is required.
| Angular version | 22.2.2 |
| Zone.js loaded? | no — zoneless application |
| Is the root component standalone? | yes |
| NgModules written in the application | 0 |
| Configured routes (top level) | 4 |
| Pages loaded lazily (one chunk each) | 75 |
imports: each component declares its dependencies
A component’s template sees only what is in its imports: other components, directives, pipes. This makes dependencies explicit, allows better tree-shaking and makes lazy loading trivial.
import { Component, input } from '@angular/core';
import { CurrencyPipe, DatePipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { Avatar } from '@shared/ui/avatar';
@Component({
selector: 'app-user-card',
// No CommonModule: @if/@for are part of the language, pipes are imported one by one
imports: [RouterLink, DatePipe, CurrencyPipe, Avatar],
template: `
<app-avatar [name]="user().name" />
<a [routerLink]="['/users', user().id]">{{ user().name }}</a>
<small>member since {{ user().joinedAt | date: 'MMM yyyy' }}</small>
`,
})
export class UserCard {
readonly user = input.required<User>();
}CommonModule was imported for *ngIf, *ngFor, ngClass and pipes. Now control flow is part of the language, [class] replaces ngClass, and you import pipes individually (DatePipe, CurrencyPipe). The result: short and precise imports.
Before and now
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { authInterceptor } from './core/auth/auth-interceptor';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(), // global errors → ErrorHandler
provideRouter(routes, withComponentInputBinding()), // router + params as input()
provideHttpClient(withInterceptors([authInterceptor])), // only if you want interceptors
// No longer needed: provideZonelessChangeDetection() (default since v21)
// No longer needed: a plain provideHttpClient() (HttpClient is in root since v21)
],
};// Before (Angular ≤ 14): everything went through an NgModule
@NgModule({
declarations: [AppComponent, HeaderComponent, UserCardComponent],
imports: [BrowserModule, HttpClientModule, RouterModule.forRoot(routes)],
providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }],
bootstrap: [AppComponent],
})
export class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);But I have libraries with NgModules…
No problem: an NgModule can go directly into the imports of a standalone component. For application-level providers from old modules there is importProvidersFrom().
// An old library that exports only an NgModule? Import it directly.
@Component({
imports: [LegacyChartsModule], // an NgModule in the imports of a standalone component — allowed
template: `<legacy-chart [data]="data()" />`,
})
export class Report {}
// A module that must add application-level providers:
export const appConfig: ApplicationConfig = {
providers: [importProvidersFrom(LegacyAnalyticsModule.forRoot({ key: '...' }))],
};Migrating an existing project
# Automatic migration to standalone (you run it in 3 steps, as the schematic asks)
ng generate @angular/core:standalone
# Other useful migrations for an old project (all exist in @angular/core v22)
ng generate @angular/core:control-flow # *ngIf/*ngFor/ngSwitch → @if/@for/@switch
ng generate @angular/core:common-to-standalone # CommonModule → individual imports
ng generate @angular/core:inject # constructor DI → inject()
ng generate @angular/core:signals # @Input/@Output/@ViewChild → input()/output()/viewChild()
ng generate @angular/core:ngclass-to-class # [ngClass] → [class]
ng generate @angular/core:ngstyle-to-style # [ngStyle] → [style]
ng generate @angular/core:service # @Injectable({providedIn:'root'}) → @Service() (v22)
ng generate @angular/core:route-lazy-loading # component: X → loadComponent: () => import(...)
ng generate @angular/core:self-closing-tag # <app-x></app-x> → <app-x />
ng generate @angular/core:cleanup-unused-importsWhen YES, when NO
- Any new code: components, directives, pipes.
- Lazy loading at component level (
loadComponent) or for a group of routes (loadChildrenwith a routes file). - Configuration through
provideX()functions instead ofModule.forRoot().
- You maintain a public library that must support very old versions of Angular.
- You have a large codebase not yet migrated — you migrate incrementally, the two models coexist.
Common pitfalls
- “X is not a known element”: you forgot to add the component to
imports. Each component imports its own children. - Unknown pipe:
dateis no longer “free” — importDatePipe. standalone: falseappears only in old/migrated code. Don’t write it in new code.- Providers you put in
app.config.tsare global. For a single feature, put them on the route (see Providers).