Standalone Components
Angular's recent releases brought a wave of new features, with Standalone Components arguably being the most significant shift. Their primary advantage is the elimination of the need for NgModules, which removes unnecessary layers of indirection and results in leaner applications. To designate a component as standalone, the standalone flag is set to true:
@Component({
standalone: true,
imports: [
NgIf,
NgForOf,
AsyncPipe,
JsonPipe,
FormsModule,
FlightCardComponent,
CityValidator,
],
selector: 'flight-search',
templateUrl: './flight-search.component.html'
})
export class FlightSearchComponent {
private store = inject(Store);
readonly flights$ = this.store.select(selectFilteredFlights);
[…]
}
In this new model, the compilation context is declared using the imports array. This array lists all the components, directives, and pipes used within the standalone component’s template. Notably, existing NgModules can also be included in this array, ensuring backward compatibility and preventing breaking changes for pre-existing code.
It’s worth noting that even the core building blocks of Angular aren't entirely free from NgModules yet. For instance, while the components within CommonModule—such as NgIf, NgFor, AsyncPipe, and JsonPipe—are now standalone, the FormsModule still relies on the traditional module structure.
The preceding example also highlights another innovation that contributes to more streamlined code: the inject function. This allows dependencies to be injected directly into class properties without the need for a dedicated constructor.
A deeper exploration of Standalone Components can be found here.
inject and the EcmaScript Standard
Using inject offers a more subtle advantage: the injected dependency can establish default values for other properties in the class. In the example, the injected store provides a default for flights$.
While this was possible with constructor-based injection before, it wasn't compliant with EcmaScript standards. According to the specification, property initializers are executed before the constructor body. While TypeScript now follows this standard, the Angular CLI historically enabled a different behavior via a compiler flag. The inject function, on the other hand, is inherently standards-compliant, ensuring your code behaves as expected without relying on specific compiler settings.
Bootstrapping a Standalone Component
Standalone components also simplify the application bootstrapping process. You now only need to pass the root AppComponent to bootstrapApplication. Global providers can be configured through a second argument:
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([authInterceptor]),
),
provideRouter(APP_ROUTES,
withPreloading(PreloadAllModules),
),
provideLogger({ debug: true },
CustomLogFormatter);
]
}
Both the HttpClient and the router now offer helper functions to set up their required global providers. This follows the provideXYZ naming convention, a pattern the Angular team recommends for third-party libraries. In the example, a custom logger library also adopts this convention, and the following section demonstrates how to build such a provider function.
Own Provide Functions
Creating custom provide functions for libraries is a straightforward process. They take configuration parameters and return an array of providers:
export function provideLogger(config: LoggerConfig,
formatterClass: Type<LogFormatter>): EnvironmentProviders {
return makeEnvironmentProviders([
{
provide: LoggerConfig,
useValue: config,
},
{
provide: LogFormatter,
useClass: formatterClass,
},
]);
}
Instead of a plain Provider[], the Angular team uses the EnvironmentProviders type for the return value. This type-driven approach restricts where these providers can be registered: they are only valid with bootstrapApplication or within router configurations, not as local providers within a component. This restriction is intentional, as libraries are typically shared across multiple parts of the application. Angular provides the makeEnvironmentProviders function to create this specific type.
For more information on crafting custom provide functions, see this article.
Functional Interceptors
Interceptors are a powerful feature of the HttpClient, allowing centralized processing of all outgoing requests and incoming responses.
In their initial incarnation, interceptors were implemented as services registered through a multi-provider. The HttpClient refactoring, needed for standalone components, introduced a more succinct alternative: functional interceptors.
These are simple functions with two arguments: the current HTTP request and a reference called next. This reference points to the next interceptor in the chain, or to the final request handler if there are none:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
if (req.url.startsWith('https://demo.angulararchitects.io/api/')) {
// Setting a dummy token for demonstration
const headers = req.headers.set('Authorization', 'Bearer Auth-1234567');
req = req.clone({headers});
}
return next(req).pipe(
tap(resp => console.log('response', resp))
);
}
To register a functional interceptor, you simply pass it to provideHttpClient, as seen in the earlier example. The need to wrap it in a service and set up a multi-provider has been completely eliminated.
Find more details on Functional Interceptors and the Standalone API for the HttpClient here.
More: Angular Architecture Workshop (online, interactive, advanced)
Enhance your expertise in creating scalable and maintainable Angular applications with our Angular Architecture workshop!

All Details (English Workshop) | All Details (German Workshop)
Functional Guards and Resolvers
Mirroring the interceptor pattern, guards and resolvers can now be implemented as plain functions. When these constructs only delegate to a service, they can be written as concise one-liners:
export const APP_ROUTES: Routes = [
[…]
{
path: 'flight-booking',
canActivate: [() => inject(AuthService).isAuthenticated()],
resolve: {
config: () => inject(ConfigService).loaded$,
},
loadChildren: () =>
import('./booking/flight-booking.routes')
// .then(m => m.FLIGHTBOOKINGROUTES)
},
[…]
]
In this example, a canActivate guard blocks unauthenticated users from accessing a route. It delegates this check to the AuthService, which is acquired via inject.
The resolver follows the same principle, delaying route activation until configuration data is loaded. It also uses inject to get its required ConfigService.
This example also showcases another Angular 15 feature that simplifies lazy loading: the loadChildren property now uses a default export. The explicit export selection, which was previously indicated in code comments, is no longer necessary.
More details on Routing With Standalone Components can be found here.
Host Directives
Introduced in Angular 15, host directives enable the integration of reusable features directly into other components and directives. These are directives imported into the host's decorator configuration, which now includes the new hostDirectives property:
@Component({
selector: 'tickets-flight-lookup',
standalone: true,
hostDirectives: [LifeCycle],
[…]
})
export class FlightLookupComponent implements OnInit {
facade = inject(FlightLookupFacade);
lifeCycle = inject(LifeCycle);
flights$ = this.facade.flights$;
ngOnInit(): void {
this.flights$.pipe(takeUntil(this.lifeCycle.destroy$))
.subscribe((v) => {
console.log('online', v);
});
}
}
Host directives share the lifecycle of the component or directive that consumes them and can be injected like standard services. This allows them to expose observables that emit on lifecycle events, which is useful for closing subscriptions using takeUntil, as demonstrated earlier. The implementation of the host directive from the previous code is shown below:
@Directive({
standalone: true,
})
export class LifeCycle implements […], OnDestroy {
[…]
private destroySubject = new Subject<void>();
readonly destroy$ = this.destroySubject.asObservable();
[…]
ngOnDestroy(): void {
this.destroySubject.next();
}
}
Another Small Step: Self-Closing Tags
A minor, yet helpful, feature for cleaner code is the support for self-closing tags. Since Angular 15.1.0, you can write:
<flight-card [item]="f" [(selected)]="basket[f.id]" />
Instead of:
<flight-card [item]="f" [(selected)]="basket[f.id]">
</flight-card>
It's a small change, but it's another piece in the mosaic of building more lightweight and readable Angular applications.
What's next? More on Architecture!
Further insights into enterprise-scale Angular architecture are available in our free eBook (5th edition, 12 chapters). It covers topics like:
- What are the criteria for dividing a large application into sub-domains?
- How can you ensure a solution remains maintainable for the long term?
- What Micro Frontend options does Module Federation provide?
Feel free to download it now.

