Angular Modules vs Standalone Components
Angular has two ways to organise and declare components, directives, and pipes: the classic NgModule system and the newer Standalone architecture introduced in Angular 14 and made the default in Angular 17.
Understanding both is essential — you will encounter NgModules in existing codebases, and you need Standalone for greenfield projects.
What Is an NgModule?
An NgModule is a class decorated with @NgModule. It acts as a compilation context: it tells Angular which components, directives, and pipes belong together, what they import, and what they export for other modules to use.
Every classic Angular app has at least one module — AppModule — which bootstraps the root component.
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { UserCardComponent } from './user-card/user-card.component';
import { SharedModule } from './shared/shared.module';
@NgModule({
declarations: [
AppComponent, // components that belong to this module
UserCardComponent,
],
imports: [
BrowserModule, // built-in Angular modules
SharedModule, // other feature modules
],
exports: [
UserCardComponent, // expose for use in other modules
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}What Is a Standalone Component?
A standalone component sets standalone: true inside its @Component decorator and manages its own imports directly — no NgModule needed.
Standalone components, directives, and pipes can import other standalone pieces or entire NgModules, giving you fine-grained control over dependencies.
// user-card.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
standalone: true, // key flag
selector: 'app-user-card',
imports: [CommonModule, RouterLink], // direct imports — no module wrapper
template: `
<div class="card">
<h3>{{ user.name }}</h3>
<a [routerLink]="['/users', user.id]">View Profile</a>
</div>
`,
})
export class UserCardComponent {
user = { id: 1, name: 'Alice' };
}Bootstrapping a Standalone Application
With standalone components you bootstrap using bootstrapApplication instead of the NgModule-based platformBrowserDynamic().bootstrapModule().
// main.ts — standalone bootstrap
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(),
],
}).catch(err => console.error(err));// app.component.ts — standalone root component
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
standalone: true,
selector: 'app-root',
imports: [RouterOutlet],
template: `<router-outlet />`,
})
export class AppComponent {}NgModule Feature Modules
In NgModule architecture, features are grouped into feature modules which are then lazy-loaded or eagerly imported by AppModule.
// products/products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductsRoutingModule } from './products-routing.module';
import { ProductListComponent } from './product-list/product-list.component';
@NgModule({
declarations: [ProductListComponent],
imports: [CommonModule, ProductsRoutingModule],
})
export class ProductsModule {}
// products/products-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
const routes: Routes = [
{ path: '', component: ProductListComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProductsRoutingModule {}Standalone Routes (No NgModule)
With standalone components, route configuration lives in a plain TypeScript file — no routing module class required.
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'products',
loadComponent: () =>
import('./products/product-list.component')
.then(m => m.ProductListComponent),
},
{
path: 'products/:id',
loadComponent: () =>
import('./products/product-detail.component')
.then(m => m.ProductDetailComponent),
},
{ path: '', redirectTo: 'products', pathMatch: 'full' },
];Side-by-Side Comparison
Aspect | NgModule | Standalone |
|---|---|---|
Declaration | Declared in @NgModule | standalone: true in @Component |
Imports | Module-level imports array | Component-level imports array |
Bootstrapping | bootstrapModule(AppModule) | bootstrapApplication(AppComponent) |
Lazy loading | loadChildren with a module | loadComponent with a component |
Boilerplate | High (extra module files) | Low (no extra files) |
Tree-shaking | Module-level granularity | Component-level (better) |
Default since | Angular 2–16 | Angular 17+ |
Mixing NgModules and Standalone
You can use standalone components inside NgModule apps and vice-versa. This makes incremental migration straightforward.
// Import a standalone component into an NgModule
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StandaloneButtonComponent, // standalone imported directly into NgModule
],
bootstrap: [AppComponent],
})
export class AppModule {}
// Import an NgModule into a standalone component
@Component({
standalone: true,
imports: [
ReactiveFormsModule, // NgModule used inside a standalone component
CommonModule,
],
template: `...`,
})
export class MyFormComponent {}Provider Scoping
Both architectures support hierarchical dependency injection. The key difference is where you register providers.
// NgModule: provide at module level
@NgModule({
providers: [ProductService],
})
export class ProductsModule {}
// Standalone: provide in bootstrapApplication
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
ProductService,
],
});
// Best practice: use providedIn: 'root' for tree-shakeable singletons
// Works for both NgModule and standalone apps
@Injectable({ providedIn: 'root' })
export class ProductService {
// available everywhere, tree-shaken when unused
}When to Use Which
New projects (Angular 17+): always use standalone — it is the default and recommended approach
Existing NgModule codebases: keep using NgModules unless you have a dedicated migration window
Libraries: standalone components are more composable and tree-shakeable for consumers
Migration path: convert leaf components to standalone first, then work upward toward the root
Use the Angular CLI schematic — ng generate @angular/core:standalone — to automate migration
ng generate component. To opt out and generate NgModule-based components, pass the --no-standalone flag.Summary: NgModules group declarations, imports, and exports at the module level; standalone components own their imports directly at the component level. Both models coexist, but standalone is the modern default. Understanding both lets you work confidently in any Angular codebase.