How to migrate Angular CoreModule to standalone APIs
Let's learn how to migrate commonly used Angular CoreModule (or any other Angular module) to standalone APIs to fully embrace standalone project setup!
Tomas Trajan
@tomastrajan
05.09.2023
9 min read
Standalone APIs are beautiful! (📸 by Kuno Schweizer)
In this article we're going to learn how to migrate commonly used Angular CoreModule (or any other Angular module) to standalone APIs!
Angular standalone components and APIs are the future! Angular CLI now allows us to generate new Angular applications with standalone setup out of the box, simply by using the new --standalone
flag when running the ng new
command!
This works really great for new greenfield projects, but we might encounter some issues in more complex enterprise environments which often tend to abstract base setup into NgModule
s in reusable libraries which are then consumed by multiple Angular apps.
Such module might prevent us from using of the standalone setup in consumer apps because we can't import such module directly in the new app.config.ts
and the provider workaround in form of importProvidersFrom
handles only providers which is often just not enough!
// 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
Before we can show how to migrate to the new standalone APIs we have to explore what kind of use cases are solved by a typical Angular
CoreModule
, and then we will be able to see how each piece of that puzzle falls into its new place!
The CoreModule
is a very common pattern in lots of existing Angular applications. It usually takes care of a large range of responsibilities with application wide impact, most typical being…
- logging / tracing
- authentication (and auth state)
- long-running processes
- translations
- main layout
- …
Let me know in the responses what are some other typical use cases that you often handle as a part of your
core/
and thereforeCoreModule
.
In general, NgModule
s are responsible for three main concerns
- register providers in the current injector of the injector hierarchy (root injector in case of the
CoreModule
and lazy injector for each lazy loaded module) - define "template context" (which components can use each other in their templates) for the components declared as a part of
declarations: [ ]
array of the given module - perform setup like loading of translations, registering of icons and kickstart long-running processes, eg reload app when new version was released, periodic upload of logs and telemetry, …
Let's explore a simplified example of a hypothetical 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 unpack what is going on through the lens of the main use case categories mentioned above…
Register providers for root injector
The CoreModule
is registering a couple of providers such as application specific implementation of the global Angular ErrorHandler
or registering of ApiKeyInterceptor
using HTTP_INTERCEPTORS
multi token.
This is the part of the implementation that will be most straight forward to migrate to the new standalone APIs!
Define template context
Main layout is usually implemented as a part of CoreModule
(or some folks might prefer to implement it in AppModule
which is very similar). The main point being it's a collection of component which are displayed right from start.
In our example, we're implementing single MainLayoutComponent
which then uses other components in its template to implement use cases like main navigation. As such it might need following collection of other components and directives imports: [NgIf, RouterLink, RouterOutlet, MatToolbarModule, MatButtonModule]
...
As we will migrate to the standalone APIs, we're going to see that this part of responsibilities will be passed on the
MainLayoutComponent
itself!
Setup and processes
The last type of the responsibilities handled by the Angular CoreModule
is to run some setup logic and kickstart some long-running processes implemented in the services.
These things can be handled in a better and more structured way using NgRx state management library but there are still many Angular applications out there without dedicated state management solution and therefore using a module to take over this responsibility became a common occurrence in many code bases.
The way this is usually done is that the CoreModule
will inject the services which implement these processes and call some of their methods to perform some setup or kickstart a process, eg this.reloadAppService.startPollNewVersionAndPromptReloadIfStale()
...
Now that we have our starting CoreModule
in place, it's a good time to explore how to migrate it to standalone APIs!
Multiple imports guard
Our CoreModule
is starting some long-running processes which should run only once per application. Because of this, it is very important to make sure that we won't accidentally import such module in lazy loaded context!
If we didn't prevent it, such import would create another instance of the module, its providers and therefore also start another instances of those long running processes which often leads to degraded performance and bugs which are hard to understand and fix!
Because of this, it is common practice to add multiple imports guard…
@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 this guard in place, if we tried to import CoreModule
in:
- a lazy loaded module (or lazy routes config)
- module in an Angular library which is the consumed by lazy loaded module (or lazy routes config) in a consumer Angular app
Then the CoreModule
will try to inject itself and throw and error if it finds an existing instance in the Angular DI context!
Such guards also works great in scenario with multiple libraries which may depend on each other all the way to the consumer Angular
The migration to standalone APIs
Migrating CoreModule
to standalone APIs means there won't be any NgModule
at the end of our migration. Instead, we're going to handle all the responsibilities in a new, better way!
Registering providers
Let's start by taking care of registering providers for the root injector. For this we're going to create a new provideCore
function which can be defined in a core.ts
file in the core/
folder (same as before with the CoreModule
)
export function provideCore(): Provider[] {
return [
// array of providers
];
}
⚠️ Please notice the explicit
Provider[]
return type, this is important because without it, the TypeScript compiler will try to infer the type as a type union of all provided providers which can break consumer build if some providers are private to the library!
With this setup, we're able to register providers the same way as we did previously in 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 },
];
}
Now we can use it in the app.config.ts
(as generated when creating new Angular application with Angular CLI and using --standalone
flag)
import { provideCore } from './core/core';
export const appConfig: ApplicationConfig = {
providers: [provideCore()],
};
What if our standalone core lives and a reusable library, and we want to make it possible to parametrize it for consumer applications?!
Parametrization and options
Let's adjust our provideCore
function by allowing consumers to pass in new options object!
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,
},
];
}
As we can see, parametrization of providers is pretty straightforward affair and proposed approach should be easy to extend to accommodate for any additional functionality delivered 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!😉
Setup and processes
We're still going to stay with the provideCore
for a little while longer before moving onto managing template context of the MainLayoutComponent
!
With provideCore
there is no constructor() {}
so how can we kickstart all the processes?!
Luckily, Angular provides us with new ENVIRONMENT_INITIALIZER
token which allows us to define logic which will run when the given injector is initialized.
Let's see how we can use it in action…
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're registering new multi provider for ENVIRONMENT_INITIALIZER
token and then provide implementation in the useValue
function body (which is a shorthand syntax for useValue: () => {}
)
Template context
The only thing left to do is to migrate our existing MainLayoutComponent
to standalone which will cause it to take on responsibility of managing its own template context which was previously done by the CoreModule
itself!
@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 last thing left to take care of is to prevent calling of the provideCore
function in multiple injectors and therefore prevent creation of multiple instances of the defined providers and their long-running processes.
Compared to the CoreModule
, we're going to need a little bit more setup in form of a dedicated injection token which will serve as a multiple provisions guard!
// 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 ...
},
},
];
}
The CORE_GUARD
is provided by the provideCore
so then if we try to provide it again (in another lazy injector), it will be able to inject already existing instance of the CORE_GUARD
and throw an error!
Supporting both module and standalone APIs
Let's imagine a situation when we're working on a library with many consumer Angular apps, and we can't just remove the MyLibModule
and replace it with standalone provideMyLib()
but have to support them side by side for some time to let consumers migrate at their own pace...
At the same time, we don't really want to maintain two separate
implementations of the same functionality, so let's see how we
can support both module and standalone APIs in a single implementation!
@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
}
}
Great, now we have fully migrated our Angular
CoreModule
to standalone APIs!
This approach can of course be extended to any other Angular module which was handling responsibilities similar to the CoreModule
discussed in this article.
Let's acknowledge that there are definitely multiple ways to go about this which will be determined by your own unique set of requirements and constrains but this example should represent a good starting point to get you going for your own migration!
Standalone APIs are great!
I hope you have enjoyed learning about how to migrate your Angular CoreModule
(and any other module) to the new standalone APIs and will implement it in your codebases.
The newly acquired know-how will be especially helpful if you work on Angular libraries within a larger organization as it will unlock ability of the Angular apps which consume some module from your library to migrate to the standalone app setup!
Also, don't hesitate to ping me if you have any questions using the article responses or Twitter DMs @tomastrajan
And never forget, future is bright
Obviously the bright Future! (📸 by Marc Zimmer )
Do you enjoy the theme of the code preview? Explore our brand new theme plugin
Skol - the ultimate IDE theme
Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.
Do you enjoy the content and would like to learn more about how to ensure long term maintainability of your Angular application?
Angular Enterprise Architecture eBook
Learn how to architect a new or existing enterprise grade Angular application with a bulletproof tooling based automated architecture validation.
This will ensure that Your project stays maintainable, extendable and therefore with high delivery velocity over the whole project lifetime!
Get notified
about new blog posts
Sign up for Angular Experts Content Updates & News and you'll get notified whenever I release a new article about Angular, Ngrx, RxJs or other interesting Frontend topics!
We will never share your email with anyone else and you can unsubscribe at any time!
Responses & comments
Do not hesitate to ask questions and share your own experience and perspective with the topic
Tomas Trajan
GDE for Angular & Web Technologies
I help developer teams deliver successful Angular applications through training and consulting with focus on NgRx, RxJs and Nx!
Ein Google-Developer expert für Angular und Webtechnologien, der als Berater und Angular-Trainer arbeitet. Derzeit unterstützt er weltweit Teams in Unternehmen bei der Implementierung von Kernfunktionalitäten, Architekturen, der Einführung von Best Practices, und der Optimierung von Arbeitsabläufen.
Tomas ist ständig bestrebt, seinen Kunden und der breiteren Entwicklergemeinschaft einen maximalen Nutzen zu bieten. Seine Arbeit wird durch eine umfangreiche Erfolgsbilanz unterstrichen, in der er populäre Fachartikel veröffentlicht, Vorträge auf internationalen Konferenzen und Meetups hält und zu Open-Source-Projekten beiträgt.
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
Check out following blog posts from Angular Experts to learn even more about related topics like Modern Angular !
Top 10 Angular Architecture Mistakes You Really Want To Avoid
In 2024, Angular keeps changing for better with ever increasing pace, but the big picture remains the same which makes architecture know-how timeless and well worth your time!
Tomas Trajan
@tomastrajan
10.09.2024
15 min read
Total guide to lazy loading with Angular @defer
Learn everything about the lazy loading of standalone components with Angular @defer block including best practices, deep dive on all interactions and gotchas including a live demo!
Tomas Trajan
@tomastrajan
14.11.2023
13 min read
Angular Control Flow
A new modern way of writing ifs, if-else, switch-cases, and even loops in Angular templates!
Kevin Kreuzer
@kreuzercode
24.10.2023
5 min read
Stärken Sie Ihr Team mit unserer umfassenden Erfahrung
Unsere Angular Experten haben viele Jahre damit verbracht, Unternehmen und Startups zu beraten, Workshops und Tutorials zu leiten und umfangreiche Open-Source-Ressourcen zu pflegen. Wir sind sehr stolz auf unsere Erfahrung im Bereich des modernen Frontends und würden uns freuen auch Ihrem Unternehmen zum Aufschwung zu verhelfen.