skip to content

In Angular, why do modules such as RouterModule expose forRoot() and forChild(), and what goes wrong if a lazy-loaded module imports forRoot()?

level: middleimportance: should knowfreq 57%

answer

  1. lazy modules get child injectors
  2. ModuleWithProviders
  3. singletons only in forRoot
  4. router throws NG04007

basics

~20 s

forRoot() returns the module plus its singleton services and global config, imported once at the root; forChild() returns it with only feature config. A lazy module importing forRoot() re-registers services in its child injector, duplicating singletons.

solid answer

~40 s

A lazily loaded NgModule gets its own child injector, so any providers it imports are registered again there, creating second instances of supposed singletons. The convention splits the module: `forRoot(config)` returns a `ModuleWithProviders` carrying the singleton services and global configuration, imported once by the root; `forChild(config)` returns the same module with only feature configuration, such as extra routes. The bare module has no providers, so importing it for its components is safe. `RouterModule.forRoot` in a lazy module would create a second `Router`; in development mode Angular throws `NG04007`, 'The Router was provided more than once'. Standalone apps use `provideRouter` and `providedIn: 'root'` instead.

code

ts · 25 lines
ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule, Routes } from '@angular/router';
import { App } from './app';
import { AdminHome } from './admin/admin-home';

const appRoutes: Routes = [
  { path: 'admin', loadChildren: () => import('./admin/admin.module').then((m) => m.AdminModule) },
];

@NgModule({
  declarations: [App],
  imports: [BrowserModule, RouterModule.forRoot(appRoutes)],
  bootstrap: [App],
})
export class AppModule {}

// admin/admin.module.ts
const adminRoutes: Routes = [{ path: '', component: AdminHome }];

@NgModule({
  declarations: [AdminHome],
  imports: [RouterModule.forChild(adminRoutes)],
})
export class AdminModule {}

go deeper

for a junior

Remember the rule: forRoot once in the root module, forChild in feature modules, and know that RouterModule is the classic example.

for a middle

Explain why: lazy modules get child injectors, forRoot returns ModuleWithProviders with singletons, and the bare module carries no providers.

for a senior

Diagnose a duplicated singleton in a lazy feature, recognise NG04007, and replace module-level providers with providedIn: 'root' or provideX functions.

for a principal

Decide when an internal library should still offer forRoot for module consumers versus moving everyone to provider functions, and plan that deprecation.

## The problem the convention solves An NgModule can carry **providers**. When a module is imported eagerly, its providers land in the application's root injector and every consumer shares one instance. But a **lazily loaded** NgModule is given its **own child injector**. If that lazy module imports a module with providers, those providers are registered again in the child injector, and code inside the lazy feature gets a **second instance** of what was meant to be a singleton. Some modules also need **configuration** — a route table, an API key, a feature flag — that should be given once for the whole app, while feature areas add only their part. The `forRoot` / `forChild` pair is the convention that handles both problems. ## How the pattern works Both are **static methods** on the module class that return a **`ModuleWithProviders<T>`** — an object with the module (`ngModule`) and a `providers` list: ```ts interface ModuleWithProviders<T> { ngModule: Type<T>; providers?: Array<Provider | EnvironmentProviders>; } ``` - **`forRoot(config)`** returns the module **plus** its singleton services and the root configuration. It is imported **once**, by the root module (or, in a standalone app, wrapped in `importProvidersFrom` in the application config). - **`forChild(config)`** returns the module with **only** the feature-level configuration — no singleton services. Feature modules, including lazy ones, import this. - The module class itself usually has **no** `providers` in its `@NgModule` metadata, so importing the bare module (for its components and directives) never duplicates services. "forRoot" and "forChild" are names chosen by convention; Angular gives the method names no special meaning. ## The canonical example: `RouterModule` | | `RouterModule.forRoot(routes, options)` | `RouterModule.forChild(routes)` | |---|---|---| | Returns | `ModuleWithProviders<RouterModule>` | `ModuleWithProviders<RouterModule>` | | Registers the `Router` and its services | Yes | No | | Registers the route table | Yes | Yes — adds this feature's routes | | Accepts global options | Yes | No | | Imported by | The root module, once | Every feature module with routes | `RouterModule` protects itself: in development mode, if `forRoot` is used in an injector below one that already has a `Router`, it throws **`NG04007`**: "The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector. Lazy loaded modules should use RouterModule.forChild() instead." In a standalone app the router is configured with `provideRouter(routes)` instead, and lazy routes use `loadChildren` returning a `Routes` array, so neither method is needed there. ## What goes wrong without it Typical bugs the pattern prevents: - **Duplicate singletons.** A lazy feature imports `AuthModule`, which lists `AuthService` in its own `providers`. The feature gets a fresh `AuthService` with no logged-in user, while the rest of the app shows the user as signed in. - **Duplicate global setup.** A second router, a second store, a second interceptor chain — often with confusing symptoms rather than a clear error. - **Configuration drift.** Two feature modules each passing their own "global" config, with the last one loaded silently changing behaviour. ## Writing your own ```ts @NgModule({ declarations: [Toast], exports: [Toast] }) export class ToastModule { static forRoot(config: ToastConfig): ModuleWithProviders<ToastModule> { return { ngModule: ToastModule, providers: [ToastService, { provide: TOAST_CONFIG, useValue: config }], }; } } ``` Guidelines: 1. Keep singleton services **out** of the `@NgModule` `providers`; put them only in `forRoot`. 2. Provide `forChild` only if features genuinely add configuration. 3. Consider a guard like the router's for services that must never be duplicated. ## The modern alternative Services decorated with `@Injectable({ providedIn: 'root' })` are registered in the root injector regardless of which module imports what, which removed most of the need for `forRoot` in application code. New libraries expose `provideX()` functions returning `EnvironmentProviders` rather than `forRoot`. You still meet `forRoot`/`forChild` in older libraries and module-based apps, which is why interviewers ask about them. ## Guarding a core module against double import Module-based apps often pair `forRoot` with a **core module** that holds app-wide singletons and must be imported exactly once. The usual guard mirrors the router's check — the module's constructor looks for an existing instance in a *parent* injector: ```ts @NgModule({ providers: [SessionStore] }) export class CoreModule { constructor() { const parent = inject(CoreModule, { optional: true, skipSelf: true }); if (parent) { throw new Error('CoreModule is already loaded; import it in AppModule only.'); } } } ``` The check turns a silent duplicate-singleton bug into a loud startup error the first time someone imports the module from a lazy feature.

  • Why does providedIn: 'root' make forRoot unnecessary for most application services?
    A service with `@Injectable({ providedIn: 'root' })` registers itself in the root injector the first time it is injected, independent of any module's `providers`. Lazy modules find it by walking up to the root, so there is nothing to duplicate. `forRoot` remains useful when a module needs configuration supplied once.
  • How does RouterModule detect that forRoot was used in the wrong place?
    In development mode, `forRoot` adds a guard provider whose factory injects `Router` with `skipSelf`. If a parent injector already has a `Router`, the factory throws `NG04007`, telling you lazy modules should use `forChild`. The check is stripped from production builds.

forRoot is like the building's main electrical supply installed once in the basement, while forChild is each floor wiring its own outlets into that supply; installing a second main supply on one floor gives that floor power that nothing else shares.

saying these in an interview costs you the question

  • forRoot and forChild are special hooks Angular calls automatically
  • A lazy-loaded module shares the root injector, so re-providing is harmless
  • forChild registers its own Router for the feature
  • Singleton services belong in the module's @NgModule providers
  • Standalone apps still need RouterModule.forRoot in the app config