DEV Community

Manish Boge
Manish Boge

Posted on • Originally published at manishboge.Medium on

From NgModules to Standalone APIs: Unlocking the Future of Angular

Unlocking the Module-Free Future of Angular with Standalone APIs

Cover

Introduction

Angular has consistently evolved, striving for simpler and more efficient ways to build robust applications. While NgModules have been the cornerstone of Angular’s architecture for years, the introduction of Standalone APIs marks a significant shift towards a more streamlined, module-free development experience.

This article will guide you through understanding, implementing, and leveraging these powerful new APIs.

The Rise of Standalone APIs in Angular

For years, NgModules were central to Angular’s architecture, organizing components, directives, pipes, and services. While effective, they eventually presented challenges: excessive boilerplate , hurdles for efficient tree-shaking , and a steep learning curve for new developers.

Standalone APIs emerged to address these issues. By allowing Angular artifacts (Components, Directives, Pipes) to be used independently of NgModules, standalone APIs offer:

  • Reduced boilerplate : Less code for declarations.
  • Improved tree-shaking : Clearer dependency graphs for better optimization.
  • Simplified learning : Easier to grasp Angular’s core concepts.
  • Enhanced developer experience : More intuitive organization and direct imports.

Here’s what we will deep dive into:

  • Standalone Components
  • Standalone Directives
  • Standalone Pipes
  • Bootstrapping Standalone App
  • Standalone App Structure
  • Providing Services in Standalone Components
  • Routing with Standalone Components
  • Lazy Loading with Standalone Components

Let’s dive into the world of Standalone APIs in Angular.

Understanding Standalone Components, Directives, and Pipes

The core of Standalone APIs lies in a new property: standalone: true.

This tells Angular: “This component doesn’t need a module.”

📌 Standalone Components

Standalone components are Angular components that can exist independently without being declared in any NgModule.

A typical component.ts file, which is a standalone, would be like below:

// src/app/components/hero-card.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-hero-card',
  standalone: true,
  imports: [],
  templateUrl: './hero-card.component.html',
  styleUrl: './hero-card.component.scss'
})
export class HeroCardComponent {
  name = 'Batman'
}
Enter fullscreen mode Exit fullscreen mode

Generate with Angular CLI:

ng generate component hero-card --standalone

# or shorthand

ng g c hero-card --standalone

# generic command

ng g c your-component-name --standalone
Enter fullscreen mode Exit fullscreen mode

Importing Dependencies

Standalone components must explicitly import other components, directives, modules, or pipes they use:

// src/app/components/hero-list.component.ts

import { Component } from '@angular/core';
import { NgIf } from '@angular/common';
import { NgFor } from '@angular/common';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  template: `
    <section class="hero-list" aria-labelledby="hero-list-heading">
      <h2 id="hero-list-heading">Hero List</h2>      
      <ul *ngIf="heros.length">
          <li *ngFor="let hero of heroes" class="hero-item">
              <span class="hero-name">{{ hero.name }}</span>
              <span class="hero-separator" aria-hidden="true">-</span>
              <span class="hero-power">{{ hero.power }}</span>
          </li>
      </ul>
    </section>
  `,
  imports: [NgIf, NgFor]
})
export class HeroListComponent {

  // sample hero data
  heroes = [
    { id: 1, name: 'Superman', power: 'Flight' },
    { id: 2, name: 'Batman', power: 'Intellect' },
    { id: 3, name: 'WonderWoman', power: 'Strength' },
    { id: 4, name: 'Flash', power: 'Speed' }
  ];
}
Enter fullscreen mode Exit fullscreen mode

We will specify the dependencies explicitly using _imports_array in component’s metadata section.

imports: [NgIf, NgFor]
Enter fullscreen mode Exit fullscreen mode

💡Note : In this example, we’ve used the *ngIf , *ngFor directives to demonstrate how dependencies are imported in a standalone component. While Angular 17 does not have stable support for the new control flow syntax , we’ll continue with *ngIf , *ngFor for now. We’ll adapt it accordingly in Angular 18. This example focuses primarily on illustrating the import mechanism in standalone components.

Key takeaways for Standalone Components:

  • standalone: true: Marks it as a standalone component.
  • imports: Replaces the imports array of an NgModule. You directly import other standalone components, directives, pipes, or even entire NgModules that your component needs.
  • No declarations needed in any NgModule.

📌 Standalone Directives

Directives can also be created as standalone units, making it easy to encapsulate and share behavior.

A typical directive.ts file, which is a standalone, would be like below:

// src/app/directives/click-logger.directive.ts

import { Directive, HostListener } from '@angular/core';

@Directive({
  selector: '[appClickLogger]',
  standalone: true,
})
export class ClickLoggerDirective {
  @HostListener('click')
  logClick() {
    console.log('Element clicked!');
  }
}
Enter fullscreen mode Exit fullscreen mode

Usage in Standalone Components

// src/app/components/hero-card.component.ts

import { Component, Input } from '@angular/core';
import { ClickLoggerDirective } from '../../directives/click-logger.directive';
import { NgIf } from '@angular/common';

@Component({
  selector: 'app-hero-card',
  standalone: true,
  imports: [NgIf, ClickLoggerDirective],
  template: `
    <div class="hero-card" *ngIf="hero" appClickLogger>
        <h3 class="hero-name">Name: {{ hero.name}}</h3>
        <p class="hero-power">Power: {{ hero.power }}</p>
    </div>
  `,
  styleUrl: './hero-card.component.scss'
})
export class HeroCardComponent { 
  hero = {
    id: 1, // Add comma here
    name: 'Batman',
    power: 'Intellect',
  }
}
Enter fullscreen mode Exit fullscreen mode

Generate with Angular CLI

ng generate directive click-logger --standalone

# or shorthand

ng g d click-logger --standalone

# generic

ng g d your-directive-name --standalone
Enter fullscreen mode Exit fullscreen mode

🧠 Use Case: Reusable UI Behavior

Think of tooltips, hover effects, click tracking — all perfect for sharing via standalone directives.

📌 Standalone Pipes

Pipes can also be defined as standalone. This makes it easy to share pure formatting logic without any module overhead.

A typical pipe.ts file, which is a standalone, would be like below:

// src/app/pipes/capitalize.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'capitalize',
  standalone: true,
})
export class CapitalizePipe implements PipeTransform {
  transform(value: string): string {
    return value.charAt(0).toUpperCase() + value.slice(1);
  }
}
Enter fullscreen mode Exit fullscreen mode

🧠 Using the Pipe in Templates

// src/app/components/hero-card.component.ts

import { Component, Input } from '@angular/core';
import { CapitalizePipe } from "../../pipes/capitalize.pipe";
import { NgIf } from '@angular/common';

@Component({
  selector: 'app-hero-card',
  standalone: true,
  imports: [NgIf, CapitalizePipe],
  template: `
    <div class="hero-card" *ngIf="hero">
        <h3 class="hero-name">Name: {{ hero.name | capitalize }}</h3>
        <p class="hero-power">Power: {{ hero.power }}</p>
    </div>
  `,
  styleUrl: './hero-card.component.scss'
})
export class HeroCardComponent { 
  hero = {
    id: 1
    name: 'Batman',
    power: 'Intellect',
  }
}
Enter fullscreen mode Exit fullscreen mode

🛠 Generate with Angular CLI

ng generate pipe capitalize --standalone

# or shorthand

ng g p capitalize --standalone

# generic

ng g p your-pipe-name --standalone
Enter fullscreen mode Exit fullscreen mode

🔁 Use Case: Shared Utility Logic

Whether it’s currency formatting, text transformation, or date manipulation — standalone pipes make code cleaner and easier to reuse.

🚀 Bootstrapping a Standalone App

With standalone components, the traditional AppModule is no longer strictly necessary for bootstrapping your application.

// src/main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient() // Provide HttpClient if your app uses it
    // Add other root-level services or providers here like provideRouter
  ]
})
.catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

No need for AppModule anymore!

📁 Standalone App Structure

One of the immediate benefits of embracing Standalone APIs is a significantly streamlined application structure. In a new standalone Angular project, you’ll notice the absence of traditional NgModule files, particularly the root app.module.ts.

Instead, your application’s entry point (main.ts) directly bootstraps a standalone root component, leveraging app.config.ts for its root-level configurations and providers. Your features are then organized with their own standalone components, directives, pipes, and services.

Here’s a typical simplified structure you might see:

src/
├── main.ts # Application entry point, bootstraps the app
└── app/
    ├── app.config.ts # Central place for root-level providers and configurations (e.g., routing)
    ├── app.component.ts # Your root standalone component
    ├── app.component.html
    ├── app.component.css
    ├── app.routes.ts # If you define your application routes separately, often imported by app.config.ts
    ├── services/ # Application-wide services (often provided via app.config.ts or providedIn: 'root')
    │ └── ...
    └── features/ # Folder for your feature-specific standalone components
        ├── feature-a/
        │ ├── feature-a.component.ts
        │ └── ...
        └── shared/ # Common standalone components/directives/pipes
            └── ...
Enter fullscreen mode Exit fullscreen mode

Providing Services with Standalone APIs

In the module-less world, how do you provide services?

providedIn: 'root' (recommended for singletons): This remains the primary way to provide application-wide singleton services.

// src/app/services/hero.service.ts
import { inject, Injectable } from '@angular/core'; 

@Injectable({
  providedIn: 'root' // This service is a singleton available throughout the app
})
export class HeroService { 
  name = 'Hero Service';
  private apiUrl = 'https://jsonplaceholder.typicode.com/users/1'; // Example API

  #http = inject(HttpClient);

  getUser(): Observable<any> {
    return this.http.get(this.apiUrl);
  }
}
Enter fullscreen mode Exit fullscreen mode

Component-level providers: You can still provide services at the component level using the providers array in the @Component decorator, which will make the service instance unique to that component and its children.

Lets visualize like for a feature, you have created a feature-component and a feature-service.

// src/app/some-feature/some-feature.component.ts
import { Component } from '@angular/core';
import { FeatureService } from './feature.service'; // A service specific to this feature

@Component({
  selector: 'app-some-feature',
  standalone: true,
  providers: [FeatureService], // Provided at component level
  template: `<p>Feature component works!</p>`
})
export class SomeFeatureComponent {
  constructor(private featureService: FeatureService) {}
}

// In service file
import { inject, Injectable } from '@angular/core';

@Injectable() // No providedIn:'root'
export class SomeFeatureComponent {
  constructor(private featureService: FeatureService) {}
}
Enter fullscreen mode Exit fullscreen mode

💡Note : The @Injectable() decorator itself doesn't inherently define scope unless providedIn: 'root' or providedIn: 'platform' is used. When an @Injectable() service (without providedIn: 'root') is listed in a component's providers array (e.g., FeatureComponent), that service instance will be scoped to that component's injector only. This means a new instance of the FeatureService will be created specifically for FeatureComponent (and its children) each time FeatureComponent is instantiated, ensuring it's not a global singleton.

Root-level providers via bootstrapApplication: As seen in main.ts, you can provide services directly when bootstrapping the application(Refer Bootstrapping Standalone App section above). This is ideal for global services like HttpClient.

Routing with Standalone Components

Routing also adapts gracefully to standalone APIs. You use new functions like provideRouter and importProvidersFrom to configure routing without relying on RouterModule.forRoot() in a traditional NgModule.

app.routes.ts structure:

// src/app/routes.ts

import { Routes } from '@angular/router';
import { UserProfileComponent } from './user-profile/user-profile.component';
import { HomeComponent } from './home/home.component'; // Assuming standalone
import { ContactComponent } from './contact/contact.component'; // Assuming standalone

export const APP_ROUTES: Routes = [
  { path: '', component: HomeComponent },
  { path: 'profile', component: UserProfileComponent },
  { path: 'contact', component: ContactComponent },
  { path: '**', redirectTo: '' } // Wildcard route
];
Enter fullscreen mode Exit fullscreen mode

app.config.ts structure:

// src/app.config.ts

import { ApplicationConfig } from '@angular/core'; // New import
import { APP_ROUTES} from './app.routes'; // Your defined routes
import { provideRouter } from '@angular/router'; // New import
import { provideHttpClient } from '@angular/common/http'; // New import

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideRouter(APP_ROUTES) // Configure routing directly,
    provideHttpClient()
  ]
};
Enter fullscreen mode Exit fullscreen mode

main.ts structure:

// src/main.ts (updated)

import { appConfig } from './app/app.config';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router'; // New import
import { UserProfileComponent } from './app/user-profile/user-profile.component';
import { APP_ROUTES } from './app/routes'; // Your defined routes

bootstrapApplication(UserProfileComponent, appConfig)
.catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

Lazy-Loading With Standalone Components

We used to lazy load the feature modules using loadChildren in module based apps, but how can we do so in a standalone app ?

_loadComponet_comes into rescue here.

// src/app/app.routes.ts

{
  path: 'hero-list',
  loadComponent: () => import('./hero-list.component').then(c => c.HeroListComponent)
}
Enter fullscreen mode Exit fullscreen mode

✅No loadChildren, no feature modules — just pure components.

🛡️Best Practices

To truly harness the power and benefits of Angular Standalone APIs, consider adopting these best practices:

📦 Prefer Granular Imports

With Standalone Components, Directives, and Pipes, you gain fine-grained control over your dependencies. Instead of importing entire NgModules, embrace the ability to import only what’s specifically needed for a particular standalone artifact.

For example:

// In your standalone component
imports: [
  CommonModule, // For NgIf, NgFor, etc. (if needed, or import them individually)
  MyStandaloneComponent,
  MyStandaloneDirective,
  MyStandalonePipe
]
Enter fullscreen mode Exit fullscreen mode

This practice directly leads to:

  • Smaller Bundles: Your final application package only includes the code it strictly requires.
  • Superior Tree-Shaking: Build tools can more effectively identify and eliminate unused code, resulting in leaner, faster applications.
  • Clearer Dependencies: It becomes immediately obvious what a component, directive, or pipe relies on, improving maintainability.

Embracing granular imports is a core philosophy of the standalone approach, reducing unnecessary boilerplate and optimizing performance.

🏁 Conclusion

Standalone APIs are the future of Angular. They:

✅ Remove unnecessary complexity

✅ Encourage better separation of concerns

✅ Make apps easier to test, lazy-load, and scale

As Angular continues to modernize, NgModules are no longer mandatory. You don’t need to refactor everything overnight — but embracing Standalone Components, Directives, and Pipes will lead to cleaner and more maintainable codebases.

If you haven’t tried Angular’s Standalone APIs yet — now is the time.

Start with a small feature or a new component and experience the simplicity and clarity that Standalone APIs bring to Angular development.

💻 Source Code & Resources

🔗 Find the complete source code for this Angular 17 series , including all examples and demos from this article, on GitHub: angular-17-series repository.

📌 Love what you see? Don’t forget to give the repo a ⭐ star to stay updated with new examples and improvements! The README.md file will guide you on running the project.

All additional resources referenced in this article are readily available for hands-on practice and learning in the repository.

🚀 Up Next: Bridging the Gap — Migrating to Standalone APIs

We’ve learnt the ‘what’ of Standalone APIs. Now, let’s tackle the ‘how.’ The upcoming article will be an essential guide to seamlessly transitioning your existing Angular applications from NgModules to the cleaner, more efficient standalone architecture. Let’s say goodbye to NgModules and get ready to modernize your codebase!!

Read

Clap

🤝Connect with Me!

Hi there! I’m Manish, a Senior Engineer passionate about building robust web applications and exploring the ever-evolving world of tech. I believe in learning and growing together.

If this article sparked your interest in modern Angular, software architecture, or just a tech chat, I’d love to connect with you!

🔗Follow me on LinkedIn for more discussions: Manish Boge

Thank you for reading — and happy 🅰️ ngularing!

Top comments (0)