@Injectable({ providedIn: 'root' })
export class UserService {
private apiUrl = '/api/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(this.apiUrl + '/' + id);
}
}
Services are injected into components, directives, pipes, or other services via constructor injection or the inject() function. Using providedIn: 'root' makes the service a singleton across the entire app.
Why it matters: Tests whether you understand Angular's core architecture principle of separation of concerns. Shows you know when and where to move logic out of components to make it reusable and testable across the application.
Real applications: UserService handles all user-related API calls and state. AuthService manages authentication logic independently. DataService fetches and caches data for multiple components. Each service has a single responsibility and is reused throughout the app.
Common mistakes: Developers often put business logic directly in components instead of services, making code hard to reuse. They forget to use providedIn: 'root' thinking services only work in NgModule providers. They also create new service instances repeatedly instead of letting DI manage the singleton.
// Provided at root level (singleton, tree-shakable)
@Injectable({ providedIn: 'root' })
export class AuthService { }
// Provided at module level
@Injectable()
export class LegacyService { }
// Must be added to a module's providers array:
// @NgModule({ providers: [LegacyService] })
// Provided at component level (new instance per component)
@Component({
providers: [LocalService]
})
export class MyComponent { }
Using providedIn: 'root' is recommended because it creates a singleton and enables tree-shaking. Unused services are automatically removed from the production bundle.
Why it matters: Tests whether you know the recommended way to provide services and understand performance implications like tree-shaking. Shows you grasp modern Angular best practices.
Real applications: Most Angular apps use providedIn: 'root' for global services like AuthService, HttpService, and DataService to ensure they're singletons across the entire app.
Common mistakes: Developers often add services to NgModule providers instead of using providedIn: 'root', missing out on tree-shakability and best practices.
@Injectable({ providedIn: 'root' })
export class CartService {
private items: Product[] = [];
addItem(product: Product) { this.items.push(product); }
getItems() { return this.items; }
getTotal() { return this.items.reduce((sum, p) => sum + p.price, 0); }
}
Benefits: 1) Singleton shared across all components. 2) Tree-shakable if unused. 3) No need to add it to any module's providers array. Alternative values include 'platform', 'any', or a specific module class.
Why it matters: Tests understanding of Angular's service lifecycle and singleton patterns. Shows you know the most efficient way to provide global services.
Real applications: ShoppingCart service registered with providedIn: 'root' becomes a singleton where every component accesses the same cart data, maintaining consistent state across the app.
Common mistakes: Developers create new service instances repeatedly or mix module-level and root-level providers, causing unexpected behavior when expecting a singleton.
// Root-level: singleton for entire app
@Injectable({ providedIn: 'root' })
export class GlobalService {}
// Module-level: shared within the module
@NgModule({
providers: [ModuleScopedService]
})
export class FeatureModule {}
// Component-level: new instance for each component
@Component({
selector: 'app-panel',
providers: [PanelService]
})
export class PanelComponent {
constructor(private panelService: PanelService) {}
}
The lookup order is: component → parent component → module → root. The first matching provider wins. Component-level providers create new instances per component.
Why it matters: Tests your deeper understanding of Angular's dependency resolution mechanism. Shows you can explain how Angular searches for providers and handles provider precedence.
Real applications: A feature module might override a global service with its own version. A component needs a local state service separate from the app-wide service, so it provides a component-level version.
Common mistakes: Developers don't realize component-level providers create new instances, thinking all components still share the same service. They also don't understand override behavior through the hierarchy.
// Define token
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
// Provide value
@NgModule({
providers: [
{ provide: API_BASE_URL, useValue: 'https://api.example.com' },
{ provide: APP_CONFIG, useValue: { debug: false, version: '1.0' } }
]
})
export class AppModule {}
// Inject with @Inject
constructor(@Inject(API_BASE_URL) private apiUrl: string) {}
InjectionTokens prevent naming collisions when multiple providers use simple types. They are essential for injecting interfaces, primitives, and configuration objects.
Why it matters: Tests whether you understand how to work with non-class dependencies and TypeScript interfaces in the DI system. Shows you know Angular patterns for configuration and complex types.
Real applications: API_BASE_URL token provides the backend URL. APP_CONFIG token provides global app settings. FEATURE_FLAGS token provides feature toggles, all injected where needed without type conflicts.
Common mistakes: Developers try to inject interfaces directly instead of using InjectionToken. They forget to use @Inject decorator when injecting tokens, confusing them with class-based services.
// useClass: provide a class (can swap implementations)
{ provide: LoggerService, useClass: DebugLoggerService }
// useValue: provide a static value
{ provide: API_URL, useValue: 'https://api.example.com' }
// useFactory: provide via a factory function
{
provide: DataService,
useFactory: (http: HttpClient, config: AppConfig) => {
return config.useMock ? new MockDataService() : new RealDataService(http);
},
deps: [HttpClient, APP_CONFIG]
}
// useExisting: alias one token to another
{ provide: AbstractLogger, useExisting: ConsoleLoggerService }
useClass creates a new instance. useValue provides a constant value. useFactory runs a function with optional deps. useExisting aliases one token to another existing provider.
Why it matters: Tests understanding of advanced provider configuration patterns. Shows you can explain different ways to customize dependency resolution and when to use each.
Real applications: useFactory is used to return different DataService (Mock vs Real) based on environment. useExisting aliases AbstractLogger to ConsoleLoggerService. useValue provides configuration objects.
Common mistakes: Developers confuse useClass with useValue, not realizing useClass instantiates the class while useValue uses it as-is. They forget to specify deps array in useFactory when the function has dependencies.
const VALIDATORS = new InjectionToken<Validator[]>('VALIDATORS');
@NgModule({
providers: [
{ provide: VALIDATORS, useClass: RequiredValidator, multi: true },
{ provide: VALIDATORS, useClass: EmailValidator, multi: true },
{ provide: VALIDATORS, useClass: MinLengthValidator, multi: true }
]
})
export class AppModule {}
// Injection returns an array
@Injectable()
export class FormService {
constructor(@Inject(VALIDATORS) private validators: Validator[]) {
// validators = [RequiredValidator, EmailValidator, MinLengthValidator]
}
}
The multi: true flag tells Angular to add the provider to a collection rather than replacing previous ones. Angular uses this pattern internally for HTTP_INTERCEPTORS and APP_INITIALIZER.
Why it matters: Tests whether you understand extensible provider patterns and real Angular framework patterns used for interceptors and initialization hooks.
Real applications: Multiple HTTP_INTERCEPTORS are registered with multi: true so each one processes requests. Multiple validators are collected into an array. Multiple APP_INITIALIZERs run before app startup.
Common mistakes: Developers forget to use multi: true and accidentally override previous validators. They don't realize multi providers collect into arrays rather than singular values.
@Injectable()
export class NotificationService {
constructor(
@Optional() private analytics: AnalyticsService
) {}
notify(msg: string) {
console.log(msg);
// Only track if analytics is available
if (this.analytics) {
this.analytics.track('notification', msg);
}
}
}
// Or using inject() function (Angular 14+)
export class MyComponent {
private analytics = inject(AnalyticsService, { optional: true });
}
@Optional is essential for building flexible components that work with or without certain services. Always check for null before using an optional dependency to avoid runtime errors.
Why it matters: Tests knowledge of advanced DI patterns and how to build robust applications that gracefully handle missing dependencies. Shows architectural thinking about optional features.
Real applications: AnalyticsService might be optional in development but required in production. A notification service works without an analytics service, just tracking silently if available. Plugin systems use optional dependencies for extensions.
Common mistakes: Developers forget to check if the optional dependency is null before using it, causing runtime errors. They think @Optional magically handles null checking, but it only prevents errors on injection.
@Component({
selector: 'app-child',
providers: [LocalService]
})
export class ChildComponent {
constructor(
// Only look at this component's injector
@Self() private local: LocalService,
// Skip this component, look at parent injectors only
@SkipSelf() private parentService: ParentService,
// Stop at the host component's injector (no further)
@Host() private hostService: HostService
) {}
}
@Self() restricts to the current injector. @SkipSelf() starts from the parent. @Host() stops at the host boundary. Combine with @Optional to avoid errors when a provider is not found at the restricted scope.
Why it matters: Tests deep understanding of Angular's injector hierarchy. Shows you can control how DI resolution works and handle advanced cases.
Real applications: A parent component provides a service, but a child component wants its own version using @Self. A dialog component using @Host restricts injection to its host. @SkipSelf skips local overrides to use parent or root versions.
Common mistakes: Developers don't understand the difference between these decorators. They use @Self when they should use @SkipSelf or vice versa. They forget @Optional goes with restriction decorators to handle missing providers gracefully.
// Tree-shakable: removed from bundle if unused
@Injectable({ providedIn: 'root' })
export class UnusedService {
doWork() { return 'work'; }
}
// NOT tree-shakable: always included because listed in module providers
@Injectable()
export class AlwaysIncludedService {
doWork() { return 'work'; }
}
@NgModule({
providers: [AlwaysIncludedService] // always in bundle
})
export class AppModule {}
With providedIn: 'root', the service references the injector rather than the injector referencing the service. This inverted dependency allows bundlers to tree-shake services that nothing imports.
Why it matters: Tests knowledge of optimal Angular patterns and build optimization. Shows you understand how modern bundlers reduce bundle size.
Real applications: Logging service never used in production is removed via tree-shaking. Analytics service not imported by any component gets eliminated. Experimental feature services get stripped from bundles if not used.
Common mistakes: Developers provide all services in modules, missing out on tree-shaking benefits and larger bundles. They don't realize tree-shaking only works with providedIn declarations, not module providers.
// Recommended: tree-shakable, singleton
@Injectable({ providedIn: 'root' })
export class GlobalService { }
// Module-level: always in bundle, not tree-shakable
@NgModule({
providers: [ModuleService]
})
export class FeatureModule { }
providedIn: 'root' is preferred for most services because of tree-shaking and singleton behavior. Use module-level providers only when you need a separate instance per lazy-loaded module.
Why it matters: Tests understanding of architectural patterns and performance implications. Shows knowledge of when to use each pattern for optimal results.
Real applications: Global AuthService uses providedIn: 'root'. Feature modules provide their own CartService for isolated checkout flows. Settings service uses root to share app-wide configuration.
Common mistakes: Developers mix both patterns in the same app. They provide in modules when they should use providedIn: 'root', losing tree-shaking. They provide in lazy modules with providedIn: 'root', creating unexpected multiple instances.
// Field-level injection
export class UserComponent {
private http = inject(HttpClient);
private router = inject(Router);
private config = inject(APP_CONFIG, { optional: true });
}
// In functional guard
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isLoggedIn();
};
The inject() function must be called in an injection context (constructor, field initializer, or factory). It supports options like optional, self, and skipSelf.
Why it matters: Tests knowledge of modern Angular DI patterns introduced in Angular 14. Shows you can use functional programming and the latest framework features.
Real applications: Functional guards use inject() to access services. Factory functions use inject() to configure providers. Standalone components use inject() for cleaner dependency injection.
Common mistakes: Developers try to use inject() outside injection contexts, causing errors. They confuse inject() with basic DI, forgetting it must be in specific contexts. They don't realize inject() is the preferred pattern for functional features.
// Root level: shared singleton
@Injectable({ providedIn: 'root' })
export class CounterService { count = 0; }
// Component level: each instance gets its own CounterService
@Component({
selector: 'app-counter',
providers: [CounterService], // separate instance
template: '<p>{{ counter.count }}</p>'
})
export class CounterComponent {
constructor(public counter: CounterService) {}
}
The component-level instance is isolated from the root singleton. Other components still use the root instance unless they also provide the service at their own level.
Why it matters: Tests understanding of how Angular's hierarchical injector system works. Shows you can predict and control instance creation for different scopes.
Real applications: Dialog service provided at dialog component level has isolated state. Each modal gets its own store instance. Nested components override parent services with local versions for independence.
Common mistakes: Developers unknowingly provide the same service at multiple levels, creating unexpected instances. They assume component-level providers still share root state, causing hard-to-debug inconsistencies.
// useClass: creates NEW instance of MockApiService
{ provide: ApiService, useClass: MockApiService }
// useExisting: both tokens point to SAME instance
{ provide: AbstractLogger, useExisting: ConsoleLoggerService }
// Example: one instance shared via two tokens
providers: [
ConsoleLoggerService,
{ provide: AbstractLogger, useExisting: ConsoleLoggerService }
]
useClass is for swapping implementations (e.g., mock vs real). useExisting is for creating token aliases that share the same underlying service instance.
Why it matters: Tests knowledge of subtle but important provider patterns. Shows understanding of when to create new instances vs when to reuse existing ones.
Real applications: useClass replaces ApiService with MockApiService in tests. useExisting makes AbstractLogger an alias for ConsoleLoggerService. Both patterns are used for flexibility and interface-based programming.
Common mistakes: Developers confuse useClass with useExisting, not realizing useClass creates new instances while useExisting shares. They use useClass when they need token aliasing or vice versa.
@Injectable({ providedIn: 'root' }) // singleton
export class CartService {
private items: Product[] = [];
addItem(item: Product) { this.items.push(item); }
getItems() { return this.items; }
}
// Both components share the same CartService instance
// Changes made by one component are visible to the other
The providedIn: 'root' pattern guarantees a singleton across the app. Avoid providing the same service in lazy-loaded modules as it breaks the singleton pattern by creating separate instances.
Why it matters: Tests fundamental understanding of state management architecture in Angular. Shows you know how to prevent common bugs with shared state.
Real applications: AuthService as a singleton manages login state across the entire app. UserPreferencesService stays consistent everywhere. ShoppingCartService is a singleton so all pages see the same cart.
Common mistakes: Developers provide the same service in multiple lazy-loaded modules, accidentally creating separate instances. They rely on singletons without realizing multiple instances were created. They don't use providedIn: 'root' and miss the singleton guarantee.