All writing
10 min read

Going Standalone: Modernizing Angular Without NgModules

Standalone components changed how I structure Angular apps. A field guide to migrating incrementally without freezing feature work.

AngularStandaloneMigration

At Mofid I inherited a large Angular application built the classic way: a forest of NgModules, a shared module that imported everything, and a dependency graph nobody fully understood. Standalone components offered a way out — but you cannot stop the world to rewrite an app in production. Here is how we migrated incrementally.

What standalone actually removes

An NgModule did three jobs: it declared components, it imported dependencies, and it provided services. Standalone components fold the first two into the component itself. Instead of declaring a component in a module and importing that module elsewhere, the component states its own dependencies:

ts
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterLink } from "@angular/router";

@Component({
  selector: "app-user-card",
  standalone: true,
  imports: [CommonModule, RouterLink],
  template: `
    <a [routerLink]="['/users', user.id]">{{ user.name }}</a>
  `,
})
export class UserCardComponent {
  @Input({ required: true }) user!: User;
}

The dependency list lives next to the thing that uses it. No more hunting through a shared module to figure out why a pipe is or is not available.

Bootstrap without a root module

The migration starts at the entry point. Replace platformBrowserDynamic().bootstrapModule(AppModule) with bootstrapApplication, and move your providers into the application config:

ts
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/auth.interceptor";

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor])),
  ],
});

Migrate leaves first

The safe order is bottom-up. Start with presentational components that have no children to worry about, flip them to standalone: true, and remove them from their NgModule's declarations. The Angular schematic automates a lot of this:

bash
ng generate @angular/core:standalone

It runs in stages — convert components, prune now-empty modules, then switch bootstrap. Run one stage, commit, ship. We did this over several sprints without ever blocking feature work, because standalone and NgModule-based code interoperate cleanly. A standalone component can be imported into an old NgModule, and an NgModule's exports can be imported into a standalone component via its imports array.

Lazy routes get dramatically simpler

This was the payoff our team felt most. Lazy loading no longer needs a feature module — you point a route at a component or a route file directly:

ts
import { Routes } from "@angular/router";

export const routes: Routes = [
  {
    path: "reports",
    loadComponent: () =>
      import("./reports/reports.component").then((m) => m.ReportsComponent),
  },
  {
    path: "admin",
    loadChildren: () =>
      import("./admin/admin.routes").then((m) => m.adminRoutes),
  },
];

What I would tell my past self

  • Do not big-bang it. Incremental migration is fully supported and far less risky.
  • Delete the god SharedModule last, and replace it with a barrel file of standalone imports rather than another module.
  • Adopt the new inject() function while you are in there — it pairs naturally with standalone and cleans up constructor injection.
  • Lean on the schematic, but review every diff. It is good, not infallible, especially around providers.
Standalone did not just remove boilerplate — it made the dependency graph legible. New engineers could finally read a component and know exactly what it needed.

Angular gets unfairly maligned for ceremony, and NgModules were a big part of that reputation. Standalone components are the framework shedding weight it no longer needs — and an app that migrates to them is genuinely easier to reason about.