Skip to main content

Services & Dependency Injection

What is a Service?

A service is a class that encapsulates business logic, data access, or shared functionality that can be reused across components. Services promote the Single Responsibility Principle by separating concerns.

Creating a Service

// user.service.ts
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root', // Makes service available app-wide (singleton)
})
export class UserService {
private users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];

getUsers(): User[] {
return this.users;
}

getUserById(id: number): User | undefined {
return this.users.find(user => user.id === id);
}

addUser(user: User): void {
this.users.push(user);
}
}

interface User {
id: number;
name: string;
email: string;
}

Dependency Injection (DI)

DI is a design pattern where a class receives its dependencies from external sources rather than creating them itself.

Injecting Services

// user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
selector: 'app-user-list',
standalone: true,
template: `
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} - {{ user.email }}</li>
}
</ul>
`,
})
export class UserListComponent implements OnInit {
users: User[] = [];

// Inject service via constructor
constructor(private userService: UserService) {}

ngOnInit() {
this.users = this.userService.getUsers();
}
}

Modern Injection with inject() (Angular 14+)

import { Component, OnInit, inject } from '@angular/core';
import { UserService } from './user.service';

@Component({
selector: 'app-user-list',
standalone: true,
template: `<!-- template -->`,
})
export class UserListComponent implements OnInit {
// Inject using inject() function
private userService = inject(UserService);
users: User[] = [];

ngOnInit() {
this.users = this.userService.getUsers();
}
}

Provider Scope

Root Level (Application-wide Singleton)

@Injectable({
providedIn: 'root', // Single instance for entire app
})
export class AuthService {
private currentUser: User | null = null;

login(username: string, password: string) {
// Login logic
this.currentUser = {
id: 1,
name: username,
email: `${username}@example.com`,
};
}

logout() {
this.currentUser = null;
}

getCurrentUser(): User | null {
return this.currentUser;
}
}

Component Level (New instance per component)

@Component({
selector: 'app-shopping-cart',
standalone: true,
providers: [CartService], // New instance for this component
template: `<!-- template -->`,
})
export class ShoppingCartComponent {
constructor(private cartService: CartService) {}
}

Platform Level (Shared across multiple Angular apps)

@Injectable({
providedIn: 'platform', // Rare use case
})
export class ConfigService {}

Service Dependencies

Services can depend on other services.

// logger.service.ts
@Injectable({
providedIn: 'root',
})
export class LoggerService {
log(message: string) {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
}

error(message: string) {
console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
}
}

// api.service.ts
@Injectable({
providedIn: 'root',
})
export class ApiService {
constructor(private logger: LoggerService) {}

fetchData(url: string) {
this.logger.log(`Fetching data from: ${url}`);
// Fetch logic
}

handleError(error: Error) {
this.logger.error(error.message);
}
}

Common Service Patterns

State Management Service

import { Injectable, signal, computed } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class CartService {
// Private state
private items = signal<CartItem[]>([]);

// Public read-only state
readonly cartItems = this.items.asReadonly();

// Computed values
readonly totalItems = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);

readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);

// Actions
addItem(product: Product) {
const existingItem = this.items().find(item => item.id === product.id);

if (existingItem) {
this.items.update(items =>
items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
);
} else {
this.items.update(items => [...items, { ...product, quantity: 1 }]);
}
}

removeItem(productId: number) {
this.items.update(items => items.filter(item => item.id !== productId));
}

clearCart() {
this.items.set([]);
}
}

interface Product {
id: number;
name: string;
price: number;
}

interface CartItem extends Product {
quantity: number;
}

HTTP Service

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
providedIn: 'root',
})
export class ProductService {
private apiUrl = 'https://api.example.com/products';

constructor(private http: HttpClient) {}

getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl);
}

getProductById(id: number): Observable<Product> {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}

createProduct(product: Product): Observable<Product> {
return this.http.post<Product>(this.apiUrl, product);
}

updateProduct(id: number, product: Product): Observable<Product> {
return this.http.put<Product>(`${this.apiUrl}/${id}`, product);
}

deleteProduct(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}

Local Storage Service

@Injectable({
providedIn: 'root',
})
export class StorageService {
setItem\<T\>(key: string, value: T): void {
try {
const serializedValue = JSON.stringify(value);
localStorage.setItem(key, serializedValue);
} catch (error) {
console.error('Error saving to localStorage', error);
}
}

getItem\<T\>(key: string): T | null {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error('Error reading from localStorage', error);
return null;
}
}

removeItem(key: string): void {
localStorage.removeItem(key);
}

clear(): void {
localStorage.clear();
}
}

Injection Tokens

For non-class dependencies or configuration.

InjectionToken

// config.ts
import { InjectionToken } from '@angular/core';

export interface AppConfig {
apiUrl: string;
production: boolean;
version: string;
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

// Provide the token
import { ApplicationConfig } from '@angular/core';

export const appConfig: ApplicationConfig = {
providers: [
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
production: true,
version: '1.0.0',
},
},
],
};

// Inject the token
import { Component, inject } from '@angular/core';
import { APP_CONFIG } from './config';

@Component({
selector: 'app-root',
standalone: true,
template: `<p>API: {{ config.apiUrl }}</p>`,
})
export class AppComponent {
config = inject(APP_CONFIG);
}

Provider Types

useClass

// Provide a different class implementation
providers: [
{
provide: LoggerService,
useClass: AdvancedLoggerService,
},
];

useValue

// Provide a static value
providers: [
{
provide: 'API_URL',
useValue: 'https://api.example.com',
},
];

useFactory

// Provide a value from a factory function
export function loggerFactory(isDevelopment: boolean) {
return isDevelopment ? new ConsoleLogger() : new RemoteLogger();
}

providers: [
{
provide: LoggerService,
useFactory: loggerFactory,
deps: [IS_DEVELOPMENT],
},
];

useExisting

// Create an alias for an existing service
providers: [
LoggerService,
{
provide: 'Logger',
useExisting: LoggerService,
},
];

Advanced DI Patterns

Optional Dependencies

import { Optional } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class DataService {
constructor(@Optional() private logger?: LoggerService) {}

fetchData() {
this.logger?.log('Fetching data...');
// Fetch logic
}
}

Self and SkipSelf

import { Self, SkipSelf, Optional } from '@angular/core';

@Component({
selector: 'app-child',
standalone: true,
providers: [DataService],
})
export class ChildComponent {
constructor(
@Self() private localService: DataService, // Only from this component
@SkipSelf() private parentService: DataService, // Skip this component
@Optional() @SkipSelf() private optionalParent?: DataService
) {}
}

Service Communication

BehaviorSubject for State

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
providedIn: 'root',
})
export class ThemeService {
private themeSubject = new BehaviorSubject<'light' | 'dark'>('light');

// Expose as Observable (read-only)
theme$: Observable<'light' | 'dark'> = this.themeSubject.asObservable();

// Get current value
get currentTheme(): 'light' | 'dark' {
return this.themeSubject.value;
}

// Update theme
setTheme(theme: 'light' | 'dark') {
this.themeSubject.next(theme);
localStorage.setItem('theme', theme);
}

toggleTheme() {
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.setTheme(newTheme);
}
}

// Component usage
@Component({
selector: 'app-header',
standalone: true,
imports: [AsyncPipe],
template: `
<button (click)="toggleTheme()">Current: {{ theme$ | async }}</button>
`,
})
export class HeaderComponent {
private themeService = inject(ThemeService);
theme$ = this.themeService.theme$;

toggleTheme() {
this.themeService.toggleTheme();
}
}

Testing Services

// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';

describe('UserService', () => {
let service: UserService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UserService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should return all users', () => {
const users = service.getUsers();
expect(users.length).toBeGreaterThan(0);
});

it('should find user by id', () => {
const user = service.getUserById(1);
expect(user?.id).toBe(1);
});
});

Best Practices

✅ Do

// Use providedIn: 'root' for singleton services
@Injectable({
providedIn: 'root'
})
export class DataService {}

// Use signals for reactive state
private count = signal(0);
readonly count$ = this.count.asReadonly();

// Implement proper error handling
getData() {
return this.http.get<Data>('/api/data').pipe(
catchError(error => {
this.logger.error(error);
return throwError(() => error);
})
);
}

// Use inject() function for cleaner code
private http = inject(HttpClient);
private router = inject(Router);

❌ Don't

// Don't create services with new keyword
// ❌ Bad
const service = new UserService();

// ✅ Good - use DI
constructor(private userService: UserService) {}

// Don't use services for DOM manipulation
// ❌ Bad
@Injectable()
export class DomService {
changeColor() {
document.body.style.color = 'red';
}
}

// Don't store component-specific state in services
// unless it needs to be shared

Interview Questions

Q: What is Dependency Injection? A: DI is a design pattern where dependencies are provided to a class rather than the class creating them itself. Angular's DI system manages the creation and lifecycle of service instances.

Q: What does providedIn: 'root' do? A: It registers the service with the root injector, making it a singleton available throughout the application. It also enables tree-shaking if the service is never used.

Q: What's the difference between @Self() and @SkipSelf()? A: @Self() looks for a dependency only in the component's own injector. @SkipSelf() skips the component's injector and looks in parent injectors.

Q: When should you use a service vs. component state? A: Use services for shared state, business logic, and data access. Use component state for UI-specific state that doesn't need to be shared.

Next Steps