Angular Modules vs Standalone Components — Angular 18 Architecture

Angular 18’s standalone components are the modern way to build Angular applications. Instead of declaring components in NgModule classes that must track every component, pipe, and directive, standalone components declare their own dependencies directly in the component’s imports array. New Angular 18 projects created with the CLI use standalone architecture by default. Understanding the difference between the two architectures is essential — most existing Angular codebases use NgModules, but new development should use standalone.

Standalone Bootstrap and Configuration

// ── main.ts — standalone bootstrap ────────────────────────────────────────
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig }             from './app/app.config';
import { AppComponent }          from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));

// ── app.config.ts — application-level providers ───────────────────────────
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding }      from '@angular/router';
import { provideHttpClient, withInterceptors }           from '@angular/common/http';
import { provideAnimationsAsync }                        from '@angular/platform-browser/animations/async';
import { routes }                                        from './app.routes';
import { authInterceptor }                               from './core/interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes, withComponentInputBinding()),    // route params as @Input
    provideHttpClient(withInterceptors([authInterceptor])), // functional interceptors
    provideAnimationsAsync(),
  ],
};

// ── Standalone component ───────────────────────────────────────────────────
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
import { CommonModule }             from '@angular/common';
import { PostListComponent }        from './features/posts/post-list.component';

@Component({
  selector:    'app-root',
  standalone:  true,      // ← declares this as a standalone component
  imports: [              // ← imports dependencies directly (no NgModule needed)
    RouterOutlet,
    RouterLink,
    CommonModule,
    PostListComponent,    // use another standalone component by importing it
  ],
  template: `
    <nav>
      <a routerLink="/posts">Posts</a>
    </nav>
    <router-outlet />
  `,
})
export class AppComponent { }
Note: In NgModule architecture, every component, pipe, and directive must be declared in exactly one NgModule, and that module must be imported by the module where it is used. This created “module sprawl” in large applications — dozens of feature modules each with declarations, imports, and exports arrays. Standalone components eliminate this: they declare their own dependencies in imports: [] directly. The result is self-contained components that are easier to understand (no need to trace through NgModule imports) and easier to test (no TestBed module setup).
Tip: Use withComponentInputBinding() when calling provideRouter() to enable passing route parameters as @Input() properties on routed components. Instead of injecting ActivatedRoute and subscribing to params, the component receives the route parameter as a simple input: @Input() id!: string. This is cleaner and more testable — no need to mock ActivatedRoute in unit tests. Angular 18 supports this for route params, query params, and route data.
Warning: When using NgModule-based third-party libraries (like Angular Material 17 or older) with standalone components, import the entire module in the component’s imports array: imports: [MatButtonModule, MatInputModule]. Do not import the module in a non-existent AppModule. Angular Material 15+ provides standalone component APIs (import individual components like MatButton directly), but older versions require the module approach. Check the library version before deciding which import style to use.

NgModule vs Standalone Comparison

Concern NgModule Standalone (Angular 18)
Component declaration In NgModule.declarations[] standalone: true on component
Dependency import Module.imports[] → shared by all Component.imports[] → per-component
Bootstrap platformBrowserDynamic().bootstrapModule(AppModule) bootstrapApplication(AppComponent, config)
Lazy loading loadChildren: () => import(‘./module’) loadComponent: () => import(‘./component’)
Testing setup TestBed.configureTestingModule + NgModule Simpler TestBed setup, no module

Common Mistakes

Mistake 1 — Forgetting to add a component to imports[] before using it in the template

❌ Wrong — <app-post-list> in template but PostListComponent not in imports[]; NG8001 error.

✅ Correct — always add components, directives, and pipes to the using component’s imports: [] array.

Mistake 2 — Adding standalone: true but also declaring the component in an NgModule

❌ Wrong — standalone component declared in NgModule declarations[]; NG6007 compile error.

✅ Correct — standalone components are never declared in NgModules; they are imported directly.

🧠 Test Yourself

A standalone PostListComponent uses the Angular DatePipe in its template ({{ post.publishedAt | date }}). The DatePipe is not in any module. What must be done?