@NgModule({
declarations: [AppComponent, HeaderComponent],
imports: [BrowserModule, FormsModule],
providers: [DataService],
bootstrap: [AppComponent]
})
export class AppModule { }
Every Angular application has at least one root NgModule. With Angular 14+ standalone components, NgModules are optional but still widely used.
Why it matters: Tests understanding of Angular module basics. Shows you know how applications are organized.
Real applications: Every Angular project relies on NgModules or standalone setup. Understanding modules is essential for structuring large applications.
Common mistakes: Developers don't understand why modules exist. They declare components in multiple modules. They confuse NgModule with ES modules.
@NgModule({
declarations: [MyComponent, MyDirective, MyPipe],
imports: [CommonModule, FormsModule],
exports: [MyComponent, MyPipe],
providers: [DataService]
})
export class FeatureModule { }
A component can only appear in one module's declarations. To share it across modules, export it from a shared module and import that module elsewhere.
Why it matters: Tests understanding of module architecture. Shows you know declaration, import, and export roles.
Real applications: Shared UI libraries (buttons, modals), common directives across features, reusable pipes for formatting.
Common mistakes: Developers declare components in multiple modules causing errors. They don't export components when sharing them. They don't understand import/export relationships.
@NgModule({
declarations: [ProductListComponent, ProductDetailComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
exports: [ProductListComponent]
})
export class ProductsModule { }
Feature modules keep the AppModule lean and make the codebase easier to maintain. They can be eagerly or lazily loaded.
Why it matters: Tests understanding of code organization patterns. Shows you know feature-based architecture.
Real applications: Admin feature, e-commerce products catalog, user profile management, settings module.
Common mistakes: Developers put everything in AppModule. They don't understand lazy loading benefits. They forget to use CommonModule in feature modules.
@NgModule({
declarations: [SpinnerComponent, HighlightDirective, TruncatePipe],
imports: [CommonModule],
exports: [SpinnerComponent, HighlightDirective, TruncatePipe, CommonModule]
})
export class SharedModule { }
Do not put services in a shared module's providers, use providedIn: 'root' instead. Shared module providers create separate instances in lazy-loaded modules.
Why it matters: Tests understanding of service scoping and dependency injection. Shows you know singleton pattern concerns.
Real applications: AuthService available globally, DataService used across features, ConfigService for app-wide settings.
Common mistakes: Developers put services in shared module providers. They expect services to be singletons but get multiple instances. They don't use providedIn: 'root'.
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/products.module')
.then(m => m.ProductsModule)
}
];
Lazy-loaded modules get their own child injector, so services provided in them are scoped to that module. Use preloading strategies to load modules in the background after the initial load.
Why it matters: Tests understanding of lazy loading benefits and performance optimization. Shows you know module scoping implications.
Real applications: Admin dashboard lazy-loaded only for admin users, reporting feature loaded on-demand, maps module loaded when user navigates.
Common mistakes: Developers eager-load everything, defeating performance gains. They don't understand child injector scoping. They don't use preloading strategies.
forRoot() registers providers for the entire app (use in AppModule). forChild() registers routes for feature modules without re-registering providers.
// AppModule
RouterModule.forRoot(appRoutes)
// Feature module
RouterModule.forChild(featureRoutes)
Why it matters: Tests understanding of module setup patterns. Shows you know provider registration strategies.
Real applications: Root module setup with forRoot for routing and authentication, feature modules with forChild for isolated routes.
Common mistakes: Developers use forRoot in multiple modules causing duplicate providers. They don't use forChild for feature modules. They don't understand the provider registration difference.
CoreModule contains singleton services and one-time setup. It should only be imported in AppModule.
@NgModule({
providers: [AuthService, LoggerService]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parent: CoreModule) {
if (parent) throw new Error('CoreModule already loaded!');
}
}
Why it matters: Tests understanding of singleton patterns and guard mechanisms. Shows you know how to prevent accidental module duplication.
Real applications: AppModule must only import CoreModule once, preventing duplicate services for global features.
Common mistakes: Developers don't use the constructor guard and CoreModule gets imported twice. They don't use @SkipSelf and @Optional together. They don't throw an error when duplication is detected.
// SharedModule declares and exports the component
@NgModule({
declarations: [SharedButtonComponent],
exports: [SharedButtonComponent]
})
export class SharedModule { }
// Feature modules import SharedModule
@NgModule({ imports: [SharedModule] })
export class FeatureAModule { }
With standalone components in Angular 14+, this restriction no longer applies because standalone components manage their own dependencies.
Why it matters: Tests understanding of module declaration rules. Shows you know the benefits of standalone components.
Real applications: Shared button component available to any feature, modal dialogs, reusable form inputs.
Common mistakes: Developers declare components in multiple modules before using standalone. They don't understand why single declaration matters. They forget to export from shared modules.
// Root module: BrowserModule (only once)
@NgModule({ imports: [BrowserModule] })
export class AppModule { }
// Feature modules: CommonModule
@NgModule({ imports: [CommonModule] })
export class FeatureModule { }
CommonModule provides core directives and pipes without the browser platform setup. Always use it instead of BrowserModule in feature modules.
Why it matters: Tests understanding of module setup differences. Shows you know when to use each module.
Real applications: Root AppModule imports BrowserModule once, every feature module imports CommonModule for ngIf and ngFor.
Common mistakes: Developers import BrowserModule in feature modules causing errors. They don't import CommonModule thinking directives work automatically. They use BrowserModule multiple times.
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})
// Or use custom strategy
export class CustomPreloader implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>) {
return route.data?.['preload'] ? load() : of(null);
}
}
PreloadAllModules is good for small apps. Use a custom strategy for larger apps to preload only frequently accessed routes.
Why it matters: Tests understanding of performance optimization through preloading. Shows you know balancing initial load vs navigation speed.
Real applications: E-commerce preloads products and checkout modules, admin dashboards preload frequently accessed reports, SPA preloads main feature modules.
Common mistakes: Developers don't understand preloading benefits. They preload all modules harming initial load. They don't use custom strategies for selective preloading.
When a module is imported into multiple other modules, each importing module gets access to the exported declarations of the shared module. Services provided in the shared module get a separate instance in each lazy-loaded module, but a shared instance in eagerly loaded modules. To avoid multiple instances of services, use providedIn: 'root' instead of putting services in shared module providers. Component declarations remain shared across all importing modules properly.
@NgModule({
declarations: [SpinnerComponent],
exports: [SpinnerComponent] // available to all importers
// Do NOT add services here — use providedIn: 'root' instead
})
export class SharedModule { }\n\n\n\nWhy it matters: Tests understanding of module importing patterns and service scoping. Shows you know injection and declaration scope differences.
\n\nReal applications: Shared button component used by admin and user modules, directive that formats dates across features, service providing user data to multiple features.
\n\nCommon mistakes: Developers think shared services are global (they're not in lazy modules). They import same module multiple times by mistake. They don't understand scoping differences between declarations and providers.
@NgModule({
declarations: [MyComponent, MyDirective, MyPipe], // own items
imports: [CommonModule, FormsModule, SharedModule], // other modules
exports: [MyComponent] // make available to other modules
})
Only @Component, @Directive, and @Pipe classes go in declarations. Modules and services should never be placed in the declarations array.
Why it matters: Tests understanding of NgModule metadata arrays. Shows you know what each array is for.
Real applications: Every module declaration section filters what goes in declarations vs imports vs exports.
Common mistakes: Developers put NgModules in declarations. They put services in declarations. They don't understand the purpose of each metadata array.
@NgModule({
providers: [AuthService, LoggerService]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parent: CoreModule) {
if (parent) {
throw new Error('CoreModule is already loaded. Import it only in AppModule.');
}
}
}
@SkipSelf() looks for CoreModule in parent injectors only. @Optional() prevents an error if it is not found, meaning this is the first import.
Why it matters: Tests understanding of dependency injection and guard patterns. Shows you know singleton service protection.
Real applications: AuthService initialized once in CoreModule, LoggerService configured globally, HttpInterceptor setup controlled.
Common mistakes: Developers forget the constructor guard and CoreModule gets imported twice. They don't use @SkipSelf and @Optional together. They don't throw an error when duplication is detected.
@NgModule({
declarations: [ButtonComponent, TooltipDirective],
imports: [CommonModule],
exports: [
ButtonComponent, // export own declarations
TooltipDirective,
CommonModule // re-export so importers get ngIf, ngFor etc.
]
})
export class SharedModule { }
Only exported declarations are visible to importing modules. Non-exported declarations are private to the declaring module.
Why it matters: Tests understanding of module encapsulation. Shows you know export semantics and visibility scoping.
Real applications: Shared component libraries export only public UI components, internal components stay private, re-exporting CommonModule for convenience.
Common mistakes: Developers declare components but forget to export. They assume all declarations are visible (they're not without export). They export internal components unnecessarily.
// Standalone: no NgModule needed
@Component({
standalone: true,
imports: [CommonModule, FormsModule],
template: '...'
})
export class MyComponent { }
// Bootstrap without NgModule
bootstrapApplication(AppComponent, { providers: [...] });
You can mix standalone and NgModule-based components in the same application during gradual migration. The Angular CLI provides schematics to help with the migration process.
Why it matters: Tests understanding of modern Angular evolution. Shows you know the standalone components alternative.
Real applications: Migrating legacy applications component-by-component, new features use standalone while old uses modules, third-party libraries still use NgModules.
Common mistakes: Developers think NgModules are completely replaced (they're not yet). They don't know standalone components manage their own dependencies. They don't understand gradual migration is possible.