Let’s learn how to migrate commonly used Angular CoreModule (or any other Angular module) to standalone APIs to fully embrace standalone project setup!
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!
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 )