Migrating to Standalone
Standalone components remove the boilerplate of NgModule by letting each component declare its own dependencies directly. Migrating a mature codebase by hand is tedious and error-prone, so the Angular team ships an official migration schematic that rewrites declarations, bootstrapping, and routing for you in safe, ordered steps. This page walks through running that schematic, understanding each phase, and cleaning up the result so your app fully embraces the standalone world.
Why migrate
For years Angular required every component, directive, and pipe to be declared in an NgModule. That indirection made it hard to see what a component actually depended on and added ceremony to every new feature. Standalone components flip this: a component lists its own imports, can be lazy-loaded directly, and bootstraps without a root module. Modern Angular (17/18/19) generates standalone code by default, and the entire framework — provideRouter, provideHttpClient, functional guards — is built around it.
Migrating is low-risk because the schematic is incremental and idempotent. You can run one mode, verify the build, commit, and continue.
The three migration modes
The migration is exposed through ng generate @angular/core:standalone and runs in three distinct modes. You run them in order, building and testing between each.
| Mode | Flag value | What it does |
|---|---|---|
| Convert declarations | convert-to-standalone | Adds standalone: true (pre-19) to components, directives, and pipes, and moves their dependencies into imports. |
| Prune modules | prune-ngmodules | Removes NgModule classes that no longer declare anything and rewires remaining imports. |
| Bootstrap standalone | standalone-bootstrap | Switches main.ts from bootstrapModule to bootstrapApplication and converts the root module’s providers. |
Always run the modes in the order above. Pruning before converting, or bootstrapping before pruning, leaves dangling references the schematic cannot reason about.
Running the migration
Make sure your working tree is clean and your tests pass first, then start with the conversion mode.
ng generate @angular/core:standalone --mode convert-to-standalone
Output:
UPDATE src/app/users/user-card.component.ts (612 bytes)
UPDATE src/app/users/users.module.ts (430 bytes)
UPDATE src/app/app.component.ts (548 bytes)
✔ Standalone migration complete.
Run `ng build` to verify your project compiles.
After this step a typical component goes from depending on its module to importing exactly what it uses:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { UserAvatarComponent } from './user-avatar.component';
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule, RouterLink, UserAvatarComponent],
template: `
@if (user(); as u) {
<app-user-avatar [name]="u.name" />
<a [routerLink]="['/users', u.id]">{{ u.name }}</a>
}
`,
})
export class UserCardComponent {
// signal input — populated by the parent
user = input<User>();
}
Build to confirm everything still compiles, then run the next two modes:
ng build
ng generate @angular/core:standalone --mode prune-ngmodules
ng generate @angular/core:standalone --mode standalone-bootstrap
The bootstrap mode rewrites your entry point. A main.ts that previously called platformBrowserDynamic().bootstrapModule(AppModule) becomes:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { authInterceptor } from './app/core/auth.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
],
}).catch((err) => console.error(err));
Converting routes and lazy loading
The schematic rewrites RouterModule.forChild() route definitions into plain route arrays and converts module-based lazy loading. A lazily loaded feature module turns into a loadChildren that points at a route file, or loadComponent for a single page.
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'users',
loadChildren: () => import('./users/users.routes').then((m) => m.USERS_ROUTES),
},
{
path: 'about',
loadComponent: () => import('./about/about.component').then((m) => m.AboutComponent),
},
];
Functional guards and resolvers are now the norm. If you still have class-based CanActivate guards, convert them with inject():
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn() ? true : router.createUrlTree(['/login']);
};
Verifying and cleaning up
After all three modes run, do a full verification pass. The schematic is conservative, so a few manual touches usually remain.
ng build --configuration production
ng test
In Angular 19+, standalone: true is the default and the property is redundant. You can drop it everywhere with the dedicated cleanup schematic:
ng generate @angular/core:standalone --mode convert-to-standalone
ng update @angular/core --migrate-only standalone
Watch for
CUSTOM_ELEMENTS_SCHEMAorentryComponentsleft behind on old modules — these signal components the schematic could not fully trace. Search for any remaining*.module.tsfiles and delete the empty ones once nothing imports them.
Best practices
- Commit after each mode so you have a clean rollback point if a build breaks.
- Run
ng buildand your test suite between every mode rather than chaining all three blindly. - Migrate a feature library or sub-app at a time in large monorepos instead of the whole workspace at once.
- Replace shared “barrel” modules (like a
SharedModule) with direct component imports; standalone makes the dependency graph explicit. - Convert class-based guards, resolvers, and interceptors to their functional,
inject()-based equivalents as you go. - Delete leftover empty
NgModulefiles and removeRouterModule.forRootcalls onceprovideRouterowns routing. - On Angular 19+, run the cleanup schematic to strip the now-redundant
standalone: trueflags.