Discover the process of converting a standard Angular CoreModule (or any typical Angular module) into standalone APIs, paving the way for a fully standalone project configuration!
Tomas Trajan
@tomastrajan
Sep 5, 2023
9 min read
Standalone APIs are beautiful! (📸 by Kuno Schweizer)
Our focus here is on converting widely used Angular CoreModule—or any Angular module—into standalone APIs.
Standalone components and APIs represent the future of Angular! With the CLI, generating a new app that uses standalone setup is now straightforward, just pass the --standalone flag to the ng new command.
Fresh greenfield projects benefit greatly from this, but complex enterprise environments can pose challenges—they often centralize base configuration inside NgModules within shared libraries that several applications depend on.
Such a module can block standalone adoption in consuming apps, since importing it directly into the new app.config.ts isn't possible, and the provider-based fallback using importProvidersFrom only covers providers, which frequently falls short.
// app.config.ts
import { MyOrgCoreModule } from '@my-org/core';
// standalone app setup generated by Angular CLI with --standalone
export const appConfig: ApplicationConfig = {
providers: [
// providers ...
MyOrgCoreModule, // ⚠️ doesn't work ...
importProvidersFrom(MyOrgCoreModule), // ⚠️ often not enough, doesn't work
],
};
First, let's talk about the CoreModule
To understand the migration path toward the new standalone APIs, we first need to identify the scenarios that a typical Angular
CoreModuleaddresses. After that, we can map each of those scenarios to its corresponding replacement.
The CoreModule is a widely adopted pattern in many existing Angular projects. It generally handles a diverse set of responsibilities that have broad implications for the entire application. Some of the most common ones are…
- logging / tracing
- authentication (and auth state)
- long-running processes
- translations
- main layout
- …
Feel free to share in the comments any other typical tasks you tend to put inside your
core/directory, and hence into theCoreModule.
Generally speaking, NgModules take care of three primary concerns
- provide services to the current injector within the hierarchy (the root injector for the
CoreModule, or a lazy injector for each lazy-loaded module) - set up the "template context" (which components are available to each other in templates) for components listed in the
declarations: [ ]of that module - handle initialization tasks such as loading translations, registering icons, and starting background processes like checking for new app versions, sending periodic logs and telemetry, …
We'll now look at a simplified example of a fictional enterprise-grade CoreModule!
@NgModule({
declarations: [MainLayout],
imports: [NgIf, RouterLink, RouterOutlet, MatToolbarModule, MatButtonModule],
providers: [
{ provide: ErrorHandler, useClass: BackendErrorHandler },
{ provide: HTTP_INTERCEPTORS, multi: true, useClass: ApiKeyInterceptor },
{ provide: RELOAD_SERVICE_POLL_INTERVAL, useValue: 60 * 60 * 1000 },
],
})
export class CoreModule {
private coreModuleMultipleImportsGuard = inject(CoreModule, {
skipSelf: true,
optional: true,
});
private reloadAppService = inject(ReloadAppService);
private logger = inject(Logger);
constructor() {
// prevent multiple instances (and multiple executions of the processes)
if (coreModuleMultipleImportsGuard) {
throw new Error(`CoreModule can be imported only once per application
(and never in library)`);
}
this.reloadAppService.startPollNewVersionAndPromptReloadIfStale();
this.logger.debug('[CoreModule] app started'); // for smoke tests
}
}
Let’s examine this situation through the categories of core use cases outlined earlier…
Register providers for root injector
The CoreModule sets up several providers, including an application-specific version of Angular’s global ErrorHandler and the registration of ApiKeyInterceptor through the HTTP_INTERCEPTORS multi token.
When it comes to migrating to standalone APIs, this segment of the implementation will be the simplest to handle!
Define template context
The primary layout is typically built inside the CoreModule (though some developers prefer to place it in the AppModule, which is quite comparable). Essentially, it’s a set of components that render immediately when the application starts.
In this scenario, we build a single MainLayoutComponent that relies on other components in its template for features such as primary navigation. Consequently, it needs a variety of additional components and directives, like imports: [NgIf, RouterLink, RouterOutlet, MatToolbarModule, MatButtonModule]...
As we transition to standalone APIs, we’ll notice that these duties will shift directly onto the
MainLayoutComponent!
Setup and processes
The final category of responsibilities that the Angular CoreModule manages involves executing initialization logic and launching background processes implemented within services.
A more structured approach for these tasks is available through the NgRx state management library, yet many Angular apps still lack a dedicated state management system, so using a module for this purpose has become a widespread pattern in various code bases.
Typically, this involves the CoreModule injecting services that handle these processes and invoking certain methods to carry out initialization or start a task, such as this.reloadAppService.startPollNewVersionAndPromptReloadIfStale()...
With our initial CoreModule established, we can now dive into how to transition it to standalone APIs!
Multiple imports guard
Our CoreModule triggers long-running processes intended to execute only once across the whole application. Therefore, it’s crucial to prevent its accidental inclusion in a lazy-loaded context!
Without this safeguard, such an import would spawn a fresh module instance along with its providers, potentially duplicating those long-running process instances. This often results in worse performance and bugs that are difficult to trace and resolve!
That’s why it’s standard practice to incorporate a guard to prevent multiple imports…
@NgModule(/* ... */)
export class CoreModule {
private coreModuleMultipleImportsGuard = inject(CoreModule, {
skipSelf: true,
optional: true,
});
// or previously with constructor injection...
// constructor(@Optional() @SkipSelf() coreModuleGuard: CoreModule) {}
constructor() {
// prevent multiple instances (and multiple executions of the processes)
if (coreModuleMultipleImportsGuard) {
throw new Error(`CoreModule can be imported only once per application
(and never in library)`);
}
}
}
With that check in place, any attempt to bring in CoreModule will now be blocked—whether it happens inside:
- a chunk of code loaded on demand (such as lazy routes setup)
- a module coming from an Angular library, which itself gets pulled into a lazy-loaded chunk (or lazy route setup) within the consuming app
When such an import occurs, CoreModule will attempt to self-inject, and if a matching instance already exists in the Angular DI container, it will raise an error.
These guards are especially handy in environments where several libraries depend on one another, cascading all the way down to the consumer Angular application.
Moving over to standalone APIs
Shifting CoreModule onto standalone APIs means that, once the transition is complete, no NgModule will remain. Instead, every responsibility gets handled through a fresher, improved approach.
Setting up providers
First, we'll handle provider registration for the root injector. To do that, we'll set up a fresh provideCore function, placed in a new core.ts file inside the core/ directory (just like where CoreModule used to live).
export function provideCore(): Provider[] {
return [
// array of providers
];
}
⚠️ Make sure to include the explicit
Provider[]return type here. Without it, TypeScript will attempt to infer a union type from all the providers you supply, which can cause build failures for consumers if certain providers remain internal to the library.
This configuration lets us register providers just as we used to within the CoreModule…
export function provideCore(): Provider[] {
return [
{ provide: ErrorHandler, useClass: BackendErrorHandler },
{ provide: HTTP_INTERCEPTORS, multi: true, useClass: ApiKeyInterceptor },
{ provide: RELOAD_SERVICE_POLL_INTERVAL, useValue: 60 * 60 * 1000 },
];
}
This utility is now ready to be applied in app.config.ts, which is automatically produced when scaffolding a fresh Angular project through the Angular CLI with the --standalone option enabled.
import { provideCore } from './core/core';
export const appConfig: ApplicationConfig = {
providers: [provideCore()],
};
How can we let applications that consume a reusable standalone library customize it to their own needs?!
Configuring via options
Now we're going to change our provideCore function by giving consumers control through an options argument!
export interface CoreOptions {
routes: Routes;
reloadServicePollInterval?: number;
}
export function provideCore(options: CoreOptions): Provider[] {
return [
provideRouter(options.routes), // new
{ provide: ErrorHandler, useClass: BackendErrorHandler },
{ provide: HTTP_INTERCEPTORS, multi: true, useClass: ApiKeyInterceptor },
{
provide: REFRESH_SERVICE_INTERVAL, // use value from options or default
useValue: options.reloadServicePollInterval ?? 60 * 60 * 1000,
},
];
}
Provider parametrization is, as shown above, quite a straightforward task—the solution presented here should also be simple to adapt for any extra features offered by the new standalone core.
Follow me on Twitter (X) because that way you will never miss new Angular, NgRx, RxJs and NX blog posts, news and other cool frontend stuff!😉
Before we dive into managing the template context of the MainLayoutComponent, we'll hang on with provideCore just a bit longer.
Given that provideCore doesn't include a constructor() {}, what mechanism do we have to kick off all the processes?
Fortunately, Angular offers the new ENVIRONMENT_INITIALIZER token, letting us run any logic when the specific injector gets initialized.
Below we illustrate exactly how this token can be put into practice…
export function provideCore(): Provider[] {
return [
{ provide: ErrorHandler, useClass: BackendErrorHandler },
{ provide: HTTP_INTERCEPTORS, multi: true, useClass: ApiKeyInterceptor },
{ provide: RELOAD_SERVICE_POLL_INTERVAL, useValue: 60 * 60 * 1000 },
// order matters
// (especially when accessing some of the above defined providers)
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue() {
// same as in constructor of the CoreModule ...
const reloadAppService = inject(ReloadAppService);
const logger = inject(Logger);
this.reloadAppService.startPollNewVersionAndPromptReloadIfStale();
this.logger.debug('[Core] app started'); // for smoke tests
},
},
];
}
We are now adding a fresh multi provider for the ENVIRONMENT_INITIALIZER token, with the actual implementation supplied inside the useValue function—this acts as a convenient shorthand for useValue: () => {}.
Template context
The final step is converting our current MainLayoutComponent to standalone, which means it will now handle its own template context—a responsibility that the CoreModule used to manage directly.
@Component({
standalone: true, // new, mark as standalone
// import components and directives used in own template (template context)
imports: [NgIf, RouterLink, RouterOutlet, MatToolbarModule, MatButtonModule],
// same as before, eg selector, template, ...
})
export class MainLayoutComponent {}
Multiple provisions guard
The final piece is to block the provideCore function from being called in several injectors, which in turn stops the providers from spawning multiple copies with their background operations.
In contrast to the CoreModule, a bit more configuration is required here, namely a separate injection token that acts as a safeguard against repeated provisioning.
// create unique injection token for the guard
export const CORE_GUARD = new InjectionToken<string>('CORE_GUARD');
export function provideCore(): Provider[] {
return [
{ provide: CORE_GUARD, useValue: 'CORE_GUARD' },
// other providers...
// init has to be last
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue() {
const coreGuard = inject(CORE_GUARD, {
skipSelf: true,
optional: true,
});
if (coreGuard) {
throw new TypeError(`provideCore() can be used only once
per application (and never in library)`);
}
// other setup and long processes ...
},
},
];
}
A CORE_GUARD token gets registered through provideCore, meaning any attempt to register it a second time — like from a different lazy injector — will succeed in finding the pre-existing CORE_GUARD instance and raise an exception as a result.
Supporting both module and standalone APIs
Consider working on a library consumed by multiple Angular applications. Removing MyLibModule outright and swapping it out for the standalone provideMyLib() isn't feasible; instead, both options must coexist temporarily, giving consumers the freedom to migrate on their own schedule.
That said, managing two separate sets of implementation logic isn't something we'd rather do, so the goal is to find a way to offer both the module-style and standalone-style APIs while sharing the same underlying logic.
@NgModule({
providers: [provideMyLib()], // reuse implementation
imports: [
// template context only
// other modules that export declarables, ...
// whatever was not extracted to the standalone APIs
],
})
export class MyLibModule {
myLibModuleMultipleImportsGuard = inject(MyLibModule, {
skipSelf: true,
optional: true,
});
constructor() {
if (this.myLibModuleMultipleImportsGuard) {
throw new Error(`MyLibModule can be imported only once per application
(and never in library)`);
}
// init logic was extracted to the standalone APIs
}
}
Excellent — at this point, your Angular
CoreModulehas been completely migrated to standalone APIs!
Naturally, this pattern can be applied to any other Angular module that was previously responsible for similar duties as the CoreModule outlined in this guide.
It's worth noting that many possible migration paths exist, and the right choice will depend on the unique constraints and requirements of your project. Still, this example should serve as a solid foundation to kick off your own migration efforts!
Standalone APIs are fantastic!
We hope this walkthrough of migrating your Angular CoreModule (or any other module) to the modern standalone APIs was valuable, and that you'll incorporate these techniques into your own codebases.
This knowledge becomes especially useful when working on Angular libraries in a larger setup, since it enables consuming applications to adopt a standalone-based architecture while still using the modules your library provides!
If you have any further questions, feel free to reach out via the article comments or Twitter DMs @tomastrajan
And always keep in mind, the future looks bright
Clearly, the future is radiant! (📸 by Marc Zimmer )
Captivated by the look of the code preview? Check out our brand-new theme plugin
Skol - the ultimate IDE theme
Bring the aurora borealis right into your editor. An easy yet effective dark theme, visually appealing and gentle on your eyes.
Create smarter interfaces with Angular + AI
Video Course: Angular + AI
This practical course demonstrates the process of embedding AI functionality into Angular applications, leveraging Hash Brown to create intelligent and responsive user interfaces.
Work through streaming chat, tool invocation, generative UI, structured output handling, and other topics in a sequential manner.
If you value this material and want to gain deeper insights into maintaining your Angular project's long-term health, consider further exploration.
Angular Enterprise Architecture eBook
Discover how to design a new or existing Angular application at enterprise scale using automated architecture validation powered by a dependable toolchain.
As a result, your codebase will remain maintainable, extensible, and consequently deliver high throughput throughout its entire lifecycle!
Enjoying the read and eager to dive into Angular's cutting-edge Signal Forms?
Angular Signal Forms: Interactive Deep-Dive
Twelve progressive chapters, blending theory with practical exercises, walk you through Angular's latest Signal-Forms feature set.
The curriculum covers everything from fundamental form handling and validators to building bespoke controls, managing nested forms, and plotting a migration path.
Stay in the loop
for upcoming articles
Subscribe to the Angular Experts Content Updates & News feed, and we’ll send you a heads-up each time a fresh post lands on Angular, Ngrx, RxJs, or other engaging Frontend subjects!
Your email stays private — we never share it with anyone, and unsubscribing is a breeze whenever you like!
Your thoughts & feedback
Feel free to pose questions and share your insights or experiences on the matter
Tomas Trajan
Google Developer Expert (GDE)
focused on Angular & the modern Web
My focus is on empowering developer teams to build successful Angular applications through training and consulting, with a special emphasis on Architecture and State management using NgRx!
As a Google Developer Expert for Angular & Web Technologies, I work as a consultant and Angular trainer. I currently support enterprise organizations around the globe by implementing core functionality and architecture, establishing best practices, sharing knowledge, and optimizing their workflows.
Tomas is dedicated to delivering maximum value to both customers and the wider developer community. His efforts are reflected in a strong record of publishing widely-read industry articles, delivering talks at international conferences and meetups, and contributing to open-source projects.
52
Blog posts
4.7M
Blog views
3.5K
Github stars
612
Trained developers
39
Given talks
8
Capacity to eat another cake
You might also like
Take a look at these other posts from Angular Experts to dive deeper into related topics such as Modern Angular !

Angular Signal Forms: Custom Controls Without ControlValueAccessor
Build reusable Angular custom controls with FormValueControl, model(), touch events, and schema-driven validation—without writing a ControlValueAccessor.

Kevin Kreuzer
@nivekcode
Aug 12, 2026
7 min read

Angular Signal Forms: The Missing Create/Edit Pattern
Learn a practical Angular Signal Forms pattern for create and edit flows, with route-based mode, edit data loading, linkedSignal prefilling, submit branching, and validation context.

Kevin Kreuzer
@nivekcode
Aug 1, 2026
6 min read

Angular Signal Forms Essentials
Understand the core concepts behind modern Angular Forms. Learn how to create Signal Forms, wire them up in templates, use built-in and custom validators, handle cross-field validation, submit forms, and more.

Kevin Kreuzer
@nivekcode
Feb 14, 2026
12 min read
Leverage our deep expertise for your team
Through years of collaboration with both enterprises and startups, delivering workshops and tutorials, and contributing to a wealth of open source projects, Angular Experts have accumulated deep knowledge in modern front-end development. We are eager to assist in accelerating your business growth
