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!
  • angular

Let’s learn how to migrate commonly used Angular CoreModule (or any other Angular module) to standalone APIs to fully embrace standalone project setup!

How to migrate Angular CoreModule to standalone APIs
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 NgModules 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 therefore CoreModule.

In general, NgModules are responsible for three main concerns

  1. 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)
  2. 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
  3. 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

Obviously the bright Future! (📸 by Marc Zimmer )

Get the latest from Coalist

Share your email so coalist can send you guides and industry news.

    By clicking this button, you agree to our Terms of Service and Privacy Policy.

    Partners

    Swiss Made Software