Services and Advanced Dependency Injection¶
Services in Angular are a fundamental part of the framework's architecture. They provide a way to share data, functionality, and business logic across multiple components. Combined with Angular's powerful dependency injection system, services enable you to build scalable, maintainable, and testable applications.
Understanding Services¶
Services are singleton objects that encapsulate business logic, data access, and shared functionality. They are typically injected into components, other services, or Angular constructs like guards and resolvers.
Key Characteristics¶
- Singleton by default: One instance per injector
- Injectable: Can be injected into other classes
- Reusable: Share functionality across components
- Testable: Easy to mock and unit test
Basic Service Structure¶
// user.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // Makes service available application-wide
})
export class UserService {
private users: User[] = [];
getUsers(): User[] {
return this.users;
}
addUser(user: User): void {
this.users.push(user);
}
getUserById(id: number): User | undefined {
return this.users.find(user => user.id === id);
}
}
interface User {
id: number;
name: string;
email: string;
}
Creating and Using Services¶
Creating a Service with Angular CLI¶
Basic Service Implementation¶
// data.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface Product {
id: number;
name: string;
price: number;
category: string;
inStock: boolean;
}
@Injectable({
providedIn: 'root'
})
export class DataService {
private products: Product[] = [
{ id: 1, name: 'Laptop', price: 999, category: 'Electronics', inStock: true },
{ id: 2, name: 'Phone', price: 599, category: 'Electronics', inStock: false },
{ id: 3, name: 'Book', price: 29, category: 'Education', inStock: true }
];
private productsSubject = new BehaviorSubject<Product[]>(this.products);
products$ = this.productsSubject.asObservable();
getProducts(): Observable<Product[]> {
return this.products$;
}
getProductById(id: number): Product | undefined {
return this.products.find(p => p.id === id);
}
addProduct(product: Product): void {
this.products.push(product);
this.productsSubject.next([...this.products]);
}
updateProduct(updatedProduct: Product): void {
const index = this.products.findIndex(p => p.id === updatedProduct.id);
if (index !== -1) {
this.products[index] = updatedProduct;
this.productsSubject.next([...this.products]);
}
}
deleteProduct(id: number): void {
this.products = this.products.filter(p => p.id !== id);
this.productsSubject.next([...this.products]);
}
getProductsByCategory(category: string): Product[] {
return this.products.filter(p => p.category === category);
}
searchProducts(query: string): Product[] {
return this.products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
}
}
Using Services in Components¶
// product-list.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService, Product } from '../services/data.service';
@Component({
selector: 'app-product-list',
template: `
<div class="product-list">
<h2>Products</h2>
<div class="controls">
<input
[(ngModel)]="searchQuery"
(input)="onSearch()"
placeholder="Search products...">
<select [(ngModel)]="selectedCategory" (change)="onCategoryChange()">
<option value="">All Categories</option>
<option value="Electronics">Electronics</option>
<option value="Education">Education</option>
</select>
<button (click)="showAddForm = !showAddForm">
{{ showAddForm ? 'Cancel' : 'Add Product' }}
</button>
</div>
<div class="add-form" *ngIf="showAddForm">
<h3>Add New Product</h3>
<form (ngSubmit)="addProduct()">
<input [(ngModel)]="newProduct.name" placeholder="Name" required>
<input [(ngModel)]="newProduct.price" type="number" placeholder="Price" required>
<input [(ngModel)]="newProduct.category" placeholder="Category" required>
<label>
<input [(ngModel)]="newProduct.inStock" type="checkbox"> In Stock
</label>
<button type="submit">Add Product</button>
</form>
</div>
<div class="product-grid">
<div *ngFor="let product of displayedProducts" class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">{{ product.price | currency }}</p>
<p class="category">{{ product.category }}</p>
<p class="stock" [class.out-of-stock]="!product.inStock">
{{ product.inStock ? 'In Stock' : 'Out of Stock' }}
</p>
<div class="actions">
<button (click)="editProduct(product)">Edit</button>
<button (click)="deleteProduct(product.id)" class="danger">Delete</button>
</div>
</div>
</div>
</div>
`,
styles: [`
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.product-card {
border: 1px solid #ddd;
padding: 1rem;
border-radius: 4px;
}
.out-of-stock {
color: red;
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
align-items: center;
}
.add-form {
border: 1px solid #ddd;
padding: 1rem;
margin: 1rem 0;
border-radius: 4px;
}
.add-form form {
display: flex;
gap: 1rem;
align-items: center;
}
`]
})
export class ProductListComponent implements OnInit, OnDestroy {
products: Product[] = [];
displayedProducts: Product[] = [];
searchQuery = '';
selectedCategory = '';
showAddForm = false;
newProduct: Partial<Product> = {};
private subscription = new Subscription();
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.subscription.add(
this.dataService.getProducts().subscribe(products => {
this.products = products;
this.updateDisplayedProducts();
})
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
onSearch(): void {
this.updateDisplayedProducts();
}
onCategoryChange(): void {
this.updateDisplayedProducts();
}
private updateDisplayedProducts(): void {
let filtered = this.products;
if (this.searchQuery) {
filtered = this.dataService.searchProducts(this.searchQuery);
}
if (this.selectedCategory) {
filtered = filtered.filter(p => p.category === this.selectedCategory);
}
this.displayedProducts = filtered;
}
addProduct(): void {
if (this.newProduct.name && this.newProduct.price && this.newProduct.category) {
const product: Product = {
id: Date.now(), // Simple ID generation
name: this.newProduct.name,
price: this.newProduct.price,
category: this.newProduct.category,
inStock: this.newProduct.inStock || false
};
this.dataService.addProduct(product);
this.newProduct = {};
this.showAddForm = false;
}
}
editProduct(product: Product): void {
// Implementation for editing
console.log('Edit product:', product);
}
deleteProduct(id: number): void {
if (confirm('Are you sure you want to delete this product?')) {
this.dataService.deleteProduct(id);
}
}
}
Service Lifecycle¶
Singleton Services¶
// singleton.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // Singleton across the entire application
})
export class SingletonService {
private instanceId = Math.random().toString(36).substring(7);
private callCount = 0;
getInstanceId(): string {
return this.instanceId;
}
incrementCallCount(): number {
return ++this.callCount;
}
getCallCount(): number {
return this.callCount;
}
}
Component-Level Services¶
// component-scoped.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class ComponentScopedService {
private data: any[] = [];
addData(item: any): void {
this.data.push(item);
}
getData(): any[] {
return this.data;
}
clearData(): void {
this.data = [];
}
}
// Using service at component level
@Component({
selector: 'app-scoped-example',
template: `
<div>
<p>Service Instance ID: {{ getInstanceId() }}</p>
<button (click)="addItem()">Add Item</button>
<p>Items: {{ getItemCount() }}</p>
</div>
`,
providers: [ComponentScopedService] // Service instance per component
})
export class ScopedExampleComponent {
constructor(private scopedService: ComponentScopedService) {}
addItem(): void {
this.scopedService.addData(Date.now());
}
getItemCount(): number {
return this.scopedService.getData().length;
}
getInstanceId(): string {
return (this.scopedService as any).instanceId || 'N/A';
}
}
Advanced Dependency Injection Patterns¶
Interface-Based Services¶
// Define interface for service contract
export interface ILoggerService {
log(message: string): void;
error(message: string): void;
warn(message: string): void;
}
// Console logger implementation
@Injectable()
export class ConsoleLoggerService implements ILoggerService {
log(message: string): void {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
}
error(message: string): void {
console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
}
warn(message: string): void {
console.warn(`[WARN] ${new Date().toISOString()}: ${message}`);
}
}
// Remote logger implementation
@Injectable()
export class RemoteLoggerService implements ILoggerService {
log(message: string): void {
this.sendToServer('info', message);
}
error(message: string): void {
this.sendToServer('error', message);
}
warn(message: string): void {
this.sendToServer('warning', message);
}
private sendToServer(level: string, message: string): void {
// Implementation to send logs to remote server
fetch('/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ level, message, timestamp: new Date().toISOString() })
});
}
}
Service Dependencies¶
// notification.service.ts
@Injectable({
providedIn: 'root'
})
export class NotificationService {
constructor(private logger: ConsoleLoggerService) {}
showSuccess(message: string): void {
this.logger.log(`Success notification: ${message}`);
// Show success notification logic
}
showError(message: string): void {
this.logger.error(`Error notification: ${message}`);
// Show error notification logic
}
showWarning(message: string): void {
this.logger.warn(`Warning notification: ${message}`);
// Show warning notification logic
}
}
// user-management.service.ts
@Injectable({
providedIn: 'root'
})
export class UserManagementService {
private users: User[] = [];
constructor(
private notificationService: NotificationService,
private logger: ConsoleLoggerService
) {}
createUser(userData: Omit<User, 'id'>): User {
try {
const newUser: User = {
id: Date.now(),
...userData
};
this.users.push(newUser);
this.logger.log(`User created: ${newUser.name}`);
this.notificationService.showSuccess(`User ${newUser.name} created successfully`);
return newUser;
} catch (error) {
this.logger.error(`Failed to create user: ${error}`);
this.notificationService.showError('Failed to create user');
throw error;
}
}
deleteUser(id: number): boolean {
try {
const userIndex = this.users.findIndex(u => u.id === id);
if (userIndex === -1) {
this.notificationService.showWarning('User not found');
return false;
}
const deletedUser = this.users.splice(userIndex, 1)[0];
this.logger.log(`User deleted: ${deletedUser.name}`);
this.notificationService.showSuccess(`User ${deletedUser.name} deleted`);
return true;
} catch (error) {
this.logger.error(`Failed to delete user: ${error}`);
this.notificationService.showError('Failed to delete user');
return false;
}
}
}
Hierarchical Injection¶
Understanding the Injector Tree¶
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
declarations: [AppComponent, ParentComponent, ChildComponent],
imports: [BrowserModule],
providers: [
{ provide: 'APP_LEVEL_SERVICE', useValue: 'App Level Value' }
],
bootstrap: [AppComponent]
})
export class AppModule {}
// parent.component.ts
@Component({
selector: 'app-parent',
template: `
<div class="parent">
<h2>Parent Component</h2>
<p>App Service: {{ appService }}</p>
<p>Parent Service: {{ parentService }}</p>
<app-child></app-child>
</div>
`,
providers: [
{ provide: 'PARENT_LEVEL_SERVICE', useValue: 'Parent Level Value' }
]
})
export class ParentComponent {
constructor(
@Inject('APP_LEVEL_SERVICE') public appService: string,
@Inject('PARENT_LEVEL_SERVICE') public parentService: string
) {}
}
// child.component.ts
@Component({
selector: 'app-child',
template: `
<div class="child">
<h3>Child Component</h3>
<p>App Service: {{ appService }}</p>
<p>Parent Service: {{ parentService }}</p>
<p>Child Service: {{ childService }}</p>
</div>
`,
providers: [
{ provide: 'CHILD_LEVEL_SERVICE', useValue: 'Child Level Value' }
]
})
export class ChildComponent {
constructor(
@Inject('APP_LEVEL_SERVICE') public appService: string,
@Inject('PARENT_LEVEL_SERVICE') public parentService: string,
@Inject('CHILD_LEVEL_SERVICE') public childService: string
) {}
}
Resolution Modifiers¶
// resolution-modifiers.component.ts
import { Component, Optional, Self, SkipSelf, Host } from '@angular/core';
@Component({
selector: 'app-resolution-demo',
template: `
<div>
<h3>Resolution Modifiers Demo</h3>
<p>Optional Service: {{ optionalService || 'Not found' }}</p>
<p>Self Service: {{ selfService || 'Not found' }}</p>
<p>Skip Self Service: {{ skipSelfService || 'Not found' }}</p>
<p>Host Service: {{ hostService || 'Not found' }}</p>
</div>
`,
providers: [
{ provide: 'SELF_SERVICE', useValue: 'Self Service Value' }
]
})
export class ResolutionDemoComponent {
constructor(
@Optional() @Inject('NON_EXISTENT_SERVICE') public optionalService: string,
@Self() @Inject('SELF_SERVICE') public selfService: string,
@Optional() @SkipSelf() @Inject('SELF_SERVICE') public skipSelfService: string,
@Optional() @Host() @Inject('HOST_SERVICE') public hostService: string
) {}
}
Provider Configuration¶
Different Provider Types¶
// app.module.ts
import { NgModule } from '@angular/core';
// Value Provider
const API_URL = 'https://api.example.com';
// Factory Provider
function loggerFactory(): ILoggerService {
const isDevelopment = !environment.production;
return isDevelopment ? new ConsoleLoggerService() : new RemoteLoggerService();
}
// Class Provider with useClass
@NgModule({
providers: [
// Value Provider
{ provide: 'API_URL', useValue: API_URL },
// Factory Provider
{
provide: ILoggerService,
useFactory: loggerFactory,
deps: [] // Dependencies for factory function
},
// Class Provider (explicit)
{ provide: UserService, useClass: UserService },
// Class Provider (shorthand)
UserService,
// Alias Provider
{ provide: 'LOGGER', useExisting: ILoggerService },
// Factory Provider with dependencies
{
provide: 'CONFIG_SERVICE',
useFactory: (apiUrl: string, logger: ILoggerService) => {
return new ConfigService(apiUrl, logger);
},
deps: ['API_URL', ILoggerService]
}
]
})
export class AppModule {}
Environment-Based Providers¶
// environment-providers.ts
import { InjectionToken } from '@angular/core';
import { environment } from '../environments/environment';
export const API_CONFIG = new InjectionToken<ApiConfig>('api.config');
export interface ApiConfig {
baseUrl: string;
timeout: number;
retries: number;
}
export function getApiConfig(): ApiConfig {
return {
baseUrl: environment.production ? 'https://api.prod.com' : 'https://api.dev.com',
timeout: environment.production ? 30000 : 10000,
retries: environment.production ? 3 : 1
};
}
// app.module.ts
@NgModule({
providers: [
{
provide: API_CONFIG,
useFactory: getApiConfig
}
]
})
export class AppModule {}
// Using in service
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(@Inject(API_CONFIG) private config: ApiConfig) {
console.log('API Config:', this.config);
}
}
Injection Tokens¶
Creating and Using Injection Tokens¶
// tokens.ts
import { InjectionToken } from '@angular/core';
// Simple value token
export const APP_NAME = new InjectionToken<string>('app.name');
// Complex object token
export interface AppConfig {
name: string;
version: string;
features: string[];
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// Function token
export const CACHE_STORAGE = new InjectionToken<Storage>('cache.storage');
// app.module.ts
@NgModule({
providers: [
{ provide: APP_NAME, useValue: 'My Angular App' },
{
provide: APP_CONFIG,
useValue: {
name: 'My Angular App',
version: '1.0.0',
features: ['auth', 'dashboard', 'reports']
}
},
{
provide: CACHE_STORAGE,
useFactory: () => {
return typeof(Storage) !== 'undefined' ? localStorage : new Map();
}
}
]
})
export class AppModule {}
// Using tokens in services
@Injectable({
providedIn: 'root'
})
export class ConfigService {
constructor(
@Inject(APP_NAME) private appName: string,
@Inject(APP_CONFIG) private appConfig: AppConfig,
@Inject(CACHE_STORAGE) private storage: Storage
) {
console.log(`Initializing ${this.appName} v${this.appConfig.version}`);
}
getFeatures(): string[] {
return this.appConfig.features;
}
cacheValue(key: string, value: string): void {
this.storage.setItem(key, value);
}
getCachedValue(key: string): string | null {
return this.storage.getItem(key);
}
}
Multi-Providers¶
Creating Multi-Providers¶
// multi-provider.ts
import { InjectionToken } from '@angular/core';
export const VALIDATORS = new InjectionToken<Validator[]>('validators');
export interface Validator {
validate(value: any): boolean;
getErrorMessage(): string;
}
@Injectable()
export class EmailValidator implements Validator {
validate(value: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
}
getErrorMessage(): string {
return 'Please enter a valid email address';
}
}
@Injectable()
export class LengthValidator implements Validator {
validate(value: string): boolean {
return value && value.length >= 8;
}
getErrorMessage(): string {
return 'Value must be at least 8 characters long';
}
}
@Injectable()
export class PasswordValidator implements Validator {
validate(value: string): boolean {
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/;
return passwordRegex.test(value);
}
getErrorMessage(): string {
return 'Password must contain uppercase, lowercase, and number';
}
}
// app.module.ts
@NgModule({
providers: [
{ provide: VALIDATORS, useClass: EmailValidator, multi: true },
{ provide: VALIDATORS, useClass: LengthValidator, multi: true },
{ provide: VALIDATORS, useClass: PasswordValidator, multi: true }
]
})
export class AppModule {}
// validation.service.ts
@Injectable({
providedIn: 'root'
})
export class ValidationService {
constructor(@Inject(VALIDATORS) private validators: Validator[]) {}
validate(value: any): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
for (const validator of this.validators) {
if (!validator.validate(value)) {
errors.push(validator.getErrorMessage());
}
}
return {
isValid: errors.length === 0,
errors
};
}
}
Factory Providers¶
Advanced Factory Providers¶
// factory-providers.ts
import { InjectionToken } from '@angular/core';
export const HTTP_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>('http.interceptors');
// Factory that creates different services based on environment
export function createDataService(
http: HttpClient,
config: AppConfig,
logger: ILoggerService
): DataService {
if (config.features.includes('offline-mode')) {
return new OfflineDataService(logger);
} else {
return new OnlineDataService(http, config, logger);
}
}
// Factory with complex dependencies
export function createCacheService(
storage: Storage,
config: AppConfig
): CacheService {
const cacheConfig = {
maxSize: config.features.includes('premium') ? 1000 : 100,
ttl: config.features.includes('premium') ? 86400000 : 3600000 // 24h vs 1h
};
return new CacheService(storage, cacheConfig);
}
// app.module.ts
@NgModule({
providers: [
{
provide: DataService,
useFactory: createDataService,
deps: [HttpClient, APP_CONFIG, ILoggerService]
},
{
provide: CacheService,
useFactory: createCacheService,
deps: [CACHE_STORAGE, APP_CONFIG]
}
]
})
export class AppModule {}
Service Communication Patterns¶
Event-Driven Communication¶
// event-bus.service.ts
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
export interface AppEvent {
type: string;
payload?: any;
timestamp: Date;
}
@Injectable({
providedIn: 'root'
})
export class EventBusService {
private eventSubject = new Subject<AppEvent>();
emit(type: string, payload?: any): void {
const event: AppEvent = {
type,
payload,
timestamp: new Date()
};
this.eventSubject.next(event);
}
on(eventType: string): Observable<AppEvent> {
return this.eventSubject.pipe(
filter(event => event.type === eventType)
);
}
onMultiple(eventTypes: string[]): Observable<AppEvent> {
return this.eventSubject.pipe(
filter(event => eventTypes.includes(event.type))
);
}
}
// Using event bus in services
@Injectable({
providedIn: 'root'
})
export class OrderService {
constructor(private eventBus: EventBusService) {}
createOrder(orderData: any): void {
// Create order logic
const order = { id: Date.now(), ...orderData };
// Emit event
this.eventBus.emit('order.created', order);
}
cancelOrder(orderId: number): void {
// Cancel order logic
this.eventBus.emit('order.cancelled', { orderId });
}
}
@Injectable({
providedIn: 'root'
})
export class InventoryService {
constructor(private eventBus: EventBusService) {
this.setupEventListeners();
}
private setupEventListeners(): void {
this.eventBus.on('order.created').subscribe(event => {
console.log('Updating inventory for order:', event.payload);
// Update inventory logic
});
this.eventBus.on('order.cancelled').subscribe(event => {
console.log('Restoring inventory for cancelled order:', event.payload);
// Restore inventory logic
});
}
}
State Management Service¶
// state-management.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface AppState {
user: User | null;
theme: 'light' | 'dark';
notifications: Notification[];
loading: boolean;
}
@Injectable({
providedIn: 'root'
})
export class StateService {
private initialState: AppState = {
user: null,
theme: 'light',
notifications: [],
loading: false
};
private stateSubject = new BehaviorSubject<AppState>(this.initialState);
public state$ = this.stateSubject.asObservable();
get currentState(): AppState {
return this.stateSubject.value;
}
// User state methods
setUser(user: User | null): void {
this.updateState({ user });
}
// Theme state methods
setTheme(theme: 'light' | 'dark'): void {
this.updateState({ theme });
localStorage.setItem('theme', theme);
}
// Notification state methods
addNotification(notification: Notification): void {
const notifications = [...this.currentState.notifications, notification];
this.updateState({ notifications });
}
removeNotification(id: string): void {
const notifications = this.currentState.notifications.filter(n => n.id !== id);
this.updateState({ notifications });
}
// Loading state methods
setLoading(loading: boolean): void {
this.updateState({ loading });
}
private updateState(partial: Partial<AppState>): void {
const newState = { ...this.currentState, ...partial };
this.stateSubject.next(newState);
}
// Selectors
getUser(): Observable<User | null> {
return this.state$.pipe(map(state => state.user));
}
getTheme(): Observable<'light' | 'dark'> {
return this.state$.pipe(map(state => state.theme));
}
getNotifications(): Observable<Notification[]> {
return this.state$.pipe(map(state => state.notifications));
}
getLoadingState(): Observable<boolean> {
return this.state$.pipe(map(state => state.loading));
}
}
Testing Services¶
Unit Testing Services¶
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
import { NotificationService } from './notification.service';
import { ILoggerService } from './logger.service';
describe('UserService', () => {
let service: UserService;
let mockNotificationService: jasmine.SpyObj<NotificationService>;
let mockLoggerService: jasmine.SpyObj<ILoggerService>;
beforeEach(() => {
const notificationSpy = jasmine.createSpyObj('NotificationService',
['showSuccess', 'showError', 'showWarning']);
const loggerSpy = jasmine.createSpyObj('ILoggerService',
['log', 'error', 'warn']);
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: NotificationService, useValue: notificationSpy },
{ provide: ILoggerService, useValue: loggerSpy }
]
});
service = TestBed.inject(UserService);
mockNotificationService = TestBed.inject(NotificationService) as jasmine.SpyObj<NotificationService>;
mockLoggerService = TestBed.inject(ILoggerService) as jasmine.SpyObj<ILoggerService>;
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should create a user successfully', () => {
const userData = { name: 'John Doe', email: 'john@example.com' };
const result = service.createUser(userData);
expect(result).toBeDefined();
expect(result.name).toBe(userData.name);
expect(result.email).toBe(userData.email);
expect(result.id).toBeDefined();
expect(mockLoggerService.log).toHaveBeenCalledWith(jasmine.stringContaining('User created'));
expect(mockNotificationService.showSuccess).toHaveBeenCalled();
});
it('should delete a user successfully', () => {
const userData = { name: 'John Doe', email: 'john@example.com' };
const user = service.createUser(userData);
const result = service.deleteUser(user.id);
expect(result).toBe(true);
expect(mockLoggerService.log).toHaveBeenCalledWith(jasmine.stringContaining('User deleted'));
expect(mockNotificationService.showSuccess).toHaveBeenCalled();
});
it('should handle deleting non-existent user', () => {
const result = service.deleteUser(999);
expect(result).toBe(false);
expect(mockNotificationService.showWarning).toHaveBeenCalledWith('User not found');
});
});
Testing Services with HTTP¶
// api.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ApiService]
});
service = TestBed.inject(ApiService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch users', () => {
const mockUsers = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
it('should handle HTTP errors', () => {
service.getUsers().subscribe(
users => fail('Should have failed'),
error => expect(error.status).toBe(500)
);
const req = httpMock.expectOne('/api/users');
req.flush('Server error', { status: 500, statusText: 'Internal Server Error' });
});
});
Best Practices¶
1. Service Design Principles¶
// ✅ Good: Single Responsibility
@Injectable({
providedIn: 'root'
})
export class UserService {
// Only handles user-related operations
getUsers(): Observable<User[]> { /* ... */ }
createUser(user: User): Observable<User> { /* ... */ }
updateUser(user: User): Observable<User> { /* ... */ }
deleteUser(id: number): Observable<void> { /* ... */ }
}
// ✅ Good: Dependency Injection
@Injectable({
providedIn: 'root'
})
export class OrderService {
constructor(
private http: HttpClient,
private logger: ILoggerService,
private notification: NotificationService
) {}
}
// ❌ Bad: Multiple Responsibilities
@Injectable({
providedIn: 'root'
})
export class BadService {
// Handles users, orders, payments, notifications, etc.
getUsers(): Observable<User[]> { /* ... */ }
createOrder(order: Order): Observable<Order> { /* ... */ }
processPayment(payment: Payment): Observable<PaymentResult> { /* ... */ }
sendEmail(email: Email): Observable<void> { /* ... */ }
}
2. Error Handling¶
@Injectable({
providedIn: 'root'
})
export class RobustService {
constructor(
private http: HttpClient,
private logger: ILoggerService
) {}
getData(): Observable<any[]> {
return this.http.get<any[]>('/api/data').pipe(
retry(3),
catchError(error => {
this.logger.error('Failed to fetch data', error);
return throwError(error);
})
);
}
createItem(item: any): Observable<any> {
return this.http.post<any>('/api/items', item).pipe(
catchError(error => {
this.logger.error('Failed to create item', error);
// Return a default value or rethrow
return of(null);
})
);
}
}
3. Memory Management¶
@Injectable({
providedIn: 'root'
})
export class MemoryEfficientService implements OnDestroy {
private subscriptions = new Subscription();
private intervalId?: number;
constructor() {
// Set up any subscriptions
this.subscriptions.add(
// your subscriptions here
);
// Set up intervals
this.intervalId = window.setInterval(() => {
// periodic operations
}, 5000);
}
ngOnDestroy(): void {
this.subscriptions.unsubscribe();
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
}
4. Configuration and Environment¶
@Injectable({
providedIn: 'root'
})
export class ConfigurableService {
constructor(
@Inject(APP_CONFIG) private config: AppConfig,
@Inject('API_URL') private apiUrl: string
) {}
getEndpoint(path: string): string {
return `${this.apiUrl}/${path}`;
}
isFeatureEnabled(feature: string): boolean {
return this.config.features.includes(feature);
}
}
Summary¶
Angular services and dependency injection provide a powerful foundation for building scalable applications:
Key Concepts:
- Services: Encapsulate business logic and shared functionality
- Dependency Injection: Manages service dependencies automatically
- Hierarchical Injection: Services can be provided at different levels
- Provider Configuration: Multiple ways to configure service providers
- Injection Tokens: Type-safe tokens for non-class dependencies
Advanced Patterns:
- Multi-providers: Collect multiple implementations
- Factory providers: Create services dynamically
- Interface-based services: Abstract service contracts
- Event-driven communication: Service-to-service messaging
- State management: Centralized application state
Best Practices:
- Follow single responsibility principle
- Use TypeScript interfaces for contracts
- Implement proper error handling
- Manage memory and subscriptions
- Write comprehensive tests
- Use injection tokens for configuration
Next, we'll explore HTTP client and API communication patterns for connecting your Angular application to backend services.