Services and Dependency Injection
Understand how Angular services contain reusable logic and how Dependency Injection provides those services to components and other application features.
Lesson content
What is an Angular Service? An Angular service is a reusable TypeScript class used to share business logic, data access, state, authentication, logging, or other functionality across the application. Services help keep components small and focused on presentation and user interaction. Real-Life Example Restaurant Customer ↓ Waiter ↓ Kitchen Service ↓ Food Prepared ↓ Customer In Angular: Component ↓ Service ↓ HTTP API ↓ Response ↓ Component Why Do We Need Services? Share reusable logic between components Keep API calls outside components Manage shared application state Handle authentication and authorization Centralize logging and error handling Improve testing and maintainability Create a Service A service can be created using Angular CLI. ng generate service users/user Short form: ng g s users/user Root-Provided Angular Service Modern Angular provides the @Injectable({ providedIn: 'root' }) decorator for defining an automatically provided root service. import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class UserService { getUsers(): string[] { return [ 'Ajay', 'Sneha', 'Rahul' ]; } } By default, this service is available throughout the application. Injectable Service The established approach uses @Injectable() with providedIn: 'root' . import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class UserService { getUsers(): string[] { return [ 'Ajay', 'Sneha', 'Rahul' ]; } } Using @Injectable @Injectable({ providedIn: 'root' }) @Injectable() Standard Angular service decorator Traditional and flexible decorator Can be root-provided with providedIn Provider scope is configured explicitly Works well with inject() Supports inject() and constructor injection Simple service configuration Better when advanced configuration is required What is Dependency Injection? Dependency Injection, or DI, is a design pattern in which a class receives the objects it needs instead of creating them itself. Angular's injector creates, stores, and provides service instances automatically. Without Dependency Injection export class UserComponent { userService = new UserService(); } Problems: The component creates its own dependency The classes become tightly coupled Replacing the service becomes difficult Unit testing becomes harder With Dependency Injection export class UserComponent { private userService = inject(UserService); } Angular creates and provides the service. Dependency Injection Flow Component requests UserService ↓ Angular Injector receives request ↓ Looks for registered provider ↓ Existing instance available? ┌────────┴────────┐ │ │ Yes No │ │ ▼ ▼ Return instance Create instance ↓ Store instance ↓ Return instance Injecting a Service with inject() Modern Angular commonly uses the inject() function. import { Component, inject } from '@angular/core'; @Component({ selector: 'app-users', standalone: true, template: ` @for (user of users; track user) { <p>{{ user }}</p> } ` }) export class UsersComponent { private userService = inject(UserService); users = this.userService.getUsers(); } Constructor Injection Constructor injection remains available with injectable classes. import { Component } from '@angular/core'; @Component({ selector: 'app-users', standalone: true, template: ` @for (user of users; track user) { <p>{{ user }}</p> } ` }) export class UsersComponent { users: string[]; constructor( private userService: UserService ) { this.users = this.userService.getUsers(); } } inject() vs Constructor Injection inject() Constructor Injection Modern functional style Traditional class-based style Can be used in field initializers Dependencies are constructor parameters Useful in guards and factory functions Mainly used in classes Requires an injection context Resolved while Angular constructs the class Service with HttpClient import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; export interface User { id: number; name: string; email: string; } @Injectable({ providedIn: 'root' }) export class UserService { private http = inject(HttpClient); private apiUrl = '/api/users'; getUsers(): Observable<User[]> { return this.http.get<User[]>( this.apiUrl ); } getUser(id: number): Observable<User> { return this.http.get<User>( `${this.apiUrl}/${id}` ); } createUser( user: Omit<User, 'id'> ): Observable<User> { return this.http.post<User>( this.apiUrl, user ); } } Using the HTTP Service import { Component, inject } from '@angular/core'; import { AsyncPipe } from '@angular/common'; @Component({ selector: 'app-users', standalone: true, imports: [AsyncPipe], template: ` @if (users$ | async; as users) { @for (user of users; track user.id) { <p>{{ user.name }}</p> } } ` }) export class UsersComponent { private userService = inject(UserService); users$ = this.userService.getUsers(); } Provider A provider tells Angular how to create or return a dependency. Provider ↓ Injection Token ↓ Value, Class or Factory ↓ Dependency Instance Root-Level Provider @Injectable({ providedIn: 'root' }) export class AuthService {} A root-level provider normally creates one shared instance for the application. Benefits Available throughout the application Shared singleton instance Tree-shakable when unused No manual provider registration required Application-Level Provider A dependency can be registered in the application configuration. import { ApplicationConfig } from '@angular/core'; export const appConfig: ApplicationConfig = { providers: [ UserService ] }; Component-Level Provider Providing a service inside a component creates an instance scoped to that component and its descendants. @Component({ selector: 'app-user-editor', standalone: true, providers: [ UserDraftService ], template: ` <p>User Editor</p> ` }) export class UserEditorComponent { private draftService = inject(UserDraftService); } Component Provider Flow UserEditorComponent ↓ Own Injector ↓ Own UserDraftService Instance ↓ Available to Child Components Each instance of UserEditorComponent receives its own service instance. Route-Level Provider A service can also be scoped to a route and its child routes. export const routes: Routes = [ { path: 'admin', providers: [ AdminService ], loadComponent: () => import('./admin.component') .then( module => module.AdminComponent ) } ]; Provider Scope Comparison Provider Location Service Scope providedIn: 'root' Entire application Application configuration Entire application environment Route providers Route and its children Component providers Component and its descendants Provider Configuration Types 1. useClass Use another class for a dependency token. providers: [ { provide: LoggerService, useClass: ProductionLoggerService } ] 2. useValue Provide an existing object, primitive value, or configuration. export const APP_CONFIG = { apiUrl: '/api', production: true }; providers: [ { provide: 'APP_CONFIG', useValue: APP_CONFIG } ] 3. useFactory Create a dependency using a factory function. export function createLogger( config: AppConfig ) { return config.production ? new ProductionLogger() : new DevelopmentLogger(); } providers: [ { provide: LoggerService, useFactory: createLogger, deps: [APP_CONFIG] } ] 4. useExisting Create an alias for an existing provider. providers: [ NewLoggerService, { provide: OldLoggerService, useExisting: NewLoggerService } ] InjectionToken Use InjectionToken for dependencies that do not have a runtime class, such as interfaces, configuration objects, functions, or primitive values. import { InjectionToken } from '@angular/core'; export interface AppConfig { apiUrl: string; production: boolean; } export const APP_CONFIG = new InjectionToken<AppConfig>( 'application.config' ); Provide the Token providers: [ { provide: APP_CONFIG, useValue: { apiUrl: '/api', production: true } } ] Inject the Token export class ApiService { private config = inject(APP_CONFIG); loadData() { console.log( this.config.apiUrl ); } } Why Interfaces Cannot Be Injected Directly interface AppConfig { apiUrl: string; } TypeScript interfaces disappear after compilation, so Angular cannot use them as runtime injection tokens. Use InjectionToken instead. Hierarchical Dependency Injection Angular injectors form a hierarchy. Angular starts looking for a provider close to the requesting component and then moves upward. Component Injector ↓ Parent Element Injector ↓ Environment Injector ↓ Root Injector ↓ Provider Found The nearest matching provider is used. Provider Override Example @Component({ selector: 'app-admin', providers: [ { provide: LoggerService, useClass: AdminLoggerService } ], template: ` <app-admin-dashboard /> ` }) export class AdminComponent {} The admin component and its descendants receive AdminLoggerService , while other application areas can receive the root logger. Dependency Resolution Modifiers Modifier Purpose optional Returns null when the provider is unavailable self Searches only the current injector skipSelf Starts searching from the parent injector host Stops searching at the host boundary Optional Injection private logger = inject(LoggerService, { optional: true }); When the service is unavailable, the value is null instead of Angular throwing an error. Self Injection private localStore = inject(LocalStoreService, { self: true, optional: true }); Skip Self Injection private parentStore = inject(StoreService, { skipSelf: true }); Injection Context The inject() function can only be called inside an Angular injection context. Valid Locations Component field initializer Component constructor Service field initializer Directive or pipe construction Functional route guard Resolver Provider factory Valid Example export class UserComponent { private service = inject(UserService); } Invalid Example loadUsers() { const service = inject(UserService); } A normal method call is not automatically an injection context. Service with Shared Signal State import { computed, Injectable, signal } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class CartService { private itemsState = signal<CartItem[]>([]); readonly items = this.itemsState.asReadonly(); readonly totalItems = computed(() => this.itemsState().length ); addItem(item: CartItem) { this.itemsState.update( items => [ ...items, item ] ); } } Use Shared Signal State export class CartComponent { private cartService = inject(CartService); items = this.cartService.items; totalItems = this.cartService.totalItems; } Service Responsibilities Service Type Responsibility API service Backend communication Authentication service User login and authentication state State service Shared application state Storage service Local or session storage access Logging service Application logging Notification service Messages, alerts, or toast notifications Best Practices Keep components focused on the user interface Move reusable business logic into services Use root providers for truly application-wide services Use component or route providers when isolated state is required Use typed InjectionToken objects instead of string tokens Prefer immutable or read-only shared state Do not put every function into one large service Keep HTTP services strongly typed Avoid direct service creation with the new keyword Choose the narrowest appropriate provider scope Interview Questions What is an Angular service? A service is a reusable class that contains shared business logic, data access, state, authentication, logging, or other functionality. What is Dependency Injection? Dependency Injection is a design pattern where Angular creates and provides the dependencies required by a class instead of the class constructing them directly. What does providedIn: 'root' mean? It registers the service with the root environment injector, making one shared instance available throughout the application in normal usage. Is every Angular service always a singleton? No. A service is a singleton only within the injector that provides it. Providing the same service in a component, route, or child injector can create another instance. Why use InjectionToken? Use InjectionToken to inject values that do not have a runtime class, such as interfaces, configuration objects, functions, and primitive values. What is hierarchical Dependency Injection? Angular searches through a hierarchy of injectors and uses the nearest provider that matches the requested token. Key Point Service contains reusable logic, while Dependency Injection creates and provides the service where it is needed. Easy Formula Component ↓ Requests Dependency ↓ Angular Injector ↓ Finds Provider ↓ Creates or Reuses Instance ↓ Returns Service ↓ Component Uses Service Service = Reusable Logic Provider = Creation Instruction Token = Dependency Identity Injector = Dependency Manager inject() = Request Dependency