Skip to main content
Angular Dependency Injection

Module Overview

Estimated Time: 3-4 hours | Difficulty: Intermediate | Prerequisites: Module 3
Dependency Injection (DI) is a design pattern where objects receive their dependencies from external sources rather than creating them. Angular has a powerful DI system built-in that makes your code more testable, maintainable, and modular. What You’ll Learn:
  • Creating injectable services
  • Understanding the injector hierarchy
  • Provider configurations
  • The modern inject() function
  • InjectionTokens for configuration
  • Multi-providers and use cases

What is Dependency Injection?

┌─────────────────────────────────────────────────────────────────────────┐
│                    Without DI vs With DI                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   WITHOUT DI (Tightly Coupled)                                          │
│   ─────────────────────────────                                         │
│   class UserComponent {                                                  │
│     private userService = new UserService();  // Hard to test!         │
│     private http = new HttpClient();          // Creates own deps       │
│   }                                                                      │
│                                                                          │
│   WITH DI (Loosely Coupled)                                             │
│   ───────────────────────────                                           │
│   class UserComponent {                                                  │
│     private userService = inject(UserService); // DI provides it       │
│   }                                                                      │
│                                                                          │
│   Benefits:                                                              │
│   ✓ Easier to test (mock dependencies)                                  │
│   ✓ Loosely coupled code                                                │
│   ✓ Single instance (singleton) management                              │
│   ✓ Flexible configuration                                              │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Creating a Service

Basic Service

// user.service.ts
import { Injectable, inject } 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'  // Singleton, available everywhere
})
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: Partial<User>): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
  
  updateUser(id: number, user: Partial<User>): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${id}`, user);
  }
  
  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

Using the Service

// users.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { UserService, User } from './user.service';
import { AsyncPipe } from '@angular/common';

@Component({
  selector: 'app-users',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (users$ | async; as users) {
      <ul>
        @for (user of users; track user.id) {
          <li>{{ user.name }} - {{ user.email }}</li>
        }
      </ul>
    } @else {
      <p>Loading...</p>
    }
  `
})
export class UsersComponent implements OnInit {
  // Modern inject() function
  private userService = inject(UserService);
  
  users$!: Observable<User[]>;
  
  ngOnInit() {
    this.users$ = this.userService.getUsers();
  }
}

inject() vs Constructor Injection

Angular provides two ways to inject dependencies:

Modern: inject() Function

import { Component, inject } from '@angular/core';

@Component({...})
export class UserComponent {
  // Inject in field declaration
  private userService = inject(UserService);
  private router = inject(Router);
  private route = inject(ActivatedRoute);
  
  // Can also inject in constructor body
  constructor() {
    const http = inject(HttpClient);  // Valid here too
  }
}

Classic: Constructor Injection

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

@Component({...})
export class UserComponent {
  constructor(
    private userService: UserService,
    private router: Router,
    private route: ActivatedRoute
  ) {}
}
Recommendation: Use inject() for new code. It’s more concise, works with functional features like guards and resolvers, and doesn’t require decorator metadata for TypeScript.

Injector Hierarchy

Angular has a hierarchical injector system:
┌─────────────────────────────────────────────────────────────────────────┐
│                    Injector Hierarchy                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│                     ┌─────────────────┐                                 │
│                     │  Root Injector  │                                 │
│                     │ (Application)   │                                 │
│                     │ providedIn: root│                                 │
│                     └────────┬────────┘                                 │
│                              │                                           │
│            ┌─────────────────┼─────────────────┐                        │
│            ▼                 ▼                 ▼                        │
│   ┌────────────────┐ ┌────────────────┐ ┌────────────────┐             │
│   │ Module Injector│ │ Module Injector│ │ Module Injector│             │
│   │  (Feature A)   │ │  (Feature B)   │ │  (Feature C)   │             │
│   └───────┬────────┘ └────────────────┘ └────────────────┘             │
│           │                                                              │
│   ┌───────┼───────────────┐                                             │
│   ▼       ▼               ▼                                             │
│ ┌─────┐ ┌─────┐       ┌─────┐                                          │
│ │Comp │ │Comp │       │Comp │  ← Component injectors                   │
│ │  A  │ │  B  │       │  C  │    (providers: [...])                    │
│ └─────┘ └─────┘       └─────┘                                          │
│                                                                          │
│   Resolution Order: Component → Module → Root                            │
│   (Bubble up until found)                                               │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

providedIn Options

// Application-wide singleton (recommended)
@Injectable({ providedIn: 'root' })
export class GlobalService {}

// Platform-wide (multiple apps)
@Injectable({ providedIn: 'platform' })
export class PlatformService {}

// No automatic provision (must provide manually)
@Injectable()
export class ManualService {}

Provider Configurations

Providing in Component

@Component({
  selector: 'app-feature',
  standalone: true,
  providers: [
    FeatureService  // New instance for this component tree
  ],
  template: `...`
})
export class FeatureComponent {
  private featureService = inject(FeatureService);
}

useClass - Alternative Implementation

// Interface/abstract class
export abstract class Logger {
  abstract log(message: string): void;
}

// Real implementation
@Injectable()
export class ConsoleLogger implements Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

// Mock for testing
@Injectable()
export class MockLogger implements Logger {
  log(message: string) {
    // Do nothing
  }
}

// Provide alternative implementation
@Component({
  providers: [
    { provide: Logger, useClass: ConsoleLogger }
  ]
})

useValue - Static Values

// Provide a static value
@Component({
  providers: [
    { provide: 'API_URL', useValue: 'https://api.example.com' }
  ]
})
export class AppComponent {
  private apiUrl = inject<string>('API_URL');
}

useFactory - Dynamic Creation

// Factory function for complex initialization
export function loggerFactory(config: AppConfig): Logger {
  if (config.production) {
    return new CloudLogger();
  }
  return new ConsoleLogger();
}

@Component({
  providers: [
    {
      provide: Logger,
      useFactory: loggerFactory,
      deps: [AppConfig]  // Dependencies for factory
    }
  ]
})

useExisting - Alias

// Create an alias for an existing service
@Component({
  providers: [
    NewLogger,
    { provide: OldLogger, useExisting: NewLogger }
  ]
})

InjectionToken

For non-class dependencies, use InjectionToken:
// tokens.ts
import { InjectionToken } from '@angular/core';

export interface AppConfig {
  apiUrl: string;
  production: boolean;
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
}

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

// Default value with factory
export const API_BASE_URL = new InjectionToken<string>('api.base.url', {
  providedIn: 'root',
  factory: () => 'https://api.example.com'
});
// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_CONFIG,
      useValue: {
        apiUrl: 'https://api.example.com',
        production: true,
        features: {
          darkMode: true,
          analytics: true
        }
      }
    }
  ]
};
// Using the token
@Component({...})
export class HeaderComponent {
  private config = inject(APP_CONFIG);
  
  get showDarkMode() {
    return this.config.features.darkMode;
  }
}

Multi-Providers

Provide multiple values for the same token:
// HTTP Interceptors (common use case)
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor,
        loggingInterceptor,
        errorInterceptor
      ])
    )
  ]
};

// Custom multi-provider
export const VALIDATORS = new InjectionToken<Validator[]>('validators');

@Component({
  providers: [
    { provide: VALIDATORS, useClass: RequiredValidator, multi: true },
    { provide: VALIDATORS, useClass: EmailValidator, multi: true },
    { provide: VALIDATORS, useClass: MinLengthValidator, multi: true }
  ]
})
export class FormComponent {
  private validators = inject(VALIDATORS);  // Array of validators
}

Service Design Patterns

State Service with Signals

// state.service.ts
import { Injectable, signal, computed } from '@angular/core';

export interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  notifications: Notification[];
}

@Injectable({ providedIn: 'root' })
export class StateService {
  // Private writable signals
  private _user = signal<User | null>(null);
  private _theme = signal<'light' | 'dark'>('light');
  private _notifications = signal<Notification[]>([]);
  
  // Public read-only signals
  readonly user = this._user.asReadonly();
  readonly theme = this._theme.asReadonly();
  readonly notifications = this._notifications.asReadonly();
  
  // Computed values
  readonly isLoggedIn = computed(() => this._user() !== null);
  readonly unreadCount = computed(() => 
    this._notifications().filter(n => !n.read).length
  );
  
  // Actions
  setUser(user: User | null) {
    this._user.set(user);
  }
  
  toggleTheme() {
    this._theme.update(t => t === 'light' ? 'dark' : 'light');
  }
  
  addNotification(notification: Notification) {
    this._notifications.update(n => [...n, notification]);
  }
  
  markAsRead(id: string) {
    this._notifications.update(notifications =>
      notifications.map(n => 
        n.id === id ? { ...n, read: true } : n
      )
    );
  }
}

Repository Pattern

// base-repository.ts
export abstract class BaseRepository<T extends { id: number }> {
  protected abstract endpoint: string;
  protected http = inject(HttpClient);
  
  getAll(): Observable<T[]> {
    return this.http.get<T[]>(this.endpoint);
  }
  
  getById(id: number): Observable<T> {
    return this.http.get<T>(`${this.endpoint}/${id}`);
  }
  
  create(item: Partial<T>): Observable<T> {
    return this.http.post<T>(this.endpoint, item);
  }
  
  update(id: number, item: Partial<T>): Observable<T> {
    return this.http.put<T>(`${this.endpoint}/${id}`, item);
  }
  
  delete(id: number): Observable<void> {
    return this.http.delete<void>(`${this.endpoint}/${id}`);
  }
}

// user-repository.ts
@Injectable({ providedIn: 'root' })
export class UserRepository extends BaseRepository<User> {
  protected endpoint = '/api/users';
  
  // Add user-specific methods
  getByEmail(email: string): Observable<User> {
    return this.http.get<User>(`${this.endpoint}/email/${email}`);
  }
}

Facade Pattern

// user-facade.service.ts
@Injectable({ providedIn: 'root' })
export class UserFacade {
  private userService = inject(UserService);
  private authService = inject(AuthService);
  private notificationService = inject(NotificationService);
  
  // Expose state
  readonly currentUser = this.authService.currentUser;
  readonly isLoading = signal(false);
  
  // Unified interface for complex operations
  async login(email: string, password: string) {
    this.isLoading.set(true);
    try {
      const user = await firstValueFrom(
        this.authService.login(email, password)
      );
      this.notificationService.show('Welcome back!');
      return user;
    } catch (error) {
      this.notificationService.showError('Login failed');
      throw error;
    } finally {
      this.isLoading.set(false);
    }
  }
  
  async updateProfile(updates: Partial<User>) {
    const currentUser = this.currentUser();
    if (!currentUser) throw new Error('Not logged in');
    
    const updated = await firstValueFrom(
      this.userService.updateUser(currentUser.id, updates)
    );
    this.authService.setUser(updated);
    this.notificationService.show('Profile updated');
    return updated;
  }
}

Testing Services

// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService, User } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });
  
  afterEach(() => {
    httpMock.verify();  // Ensure no outstanding requests
  });
  
  it('should fetch users', () => {
    const mockUsers: User[] = [
      { id: 1, name: 'John', email: '[email protected]' }
    ];
    
    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 create a user', () => {
    const newUser = { name: 'Jane', email: '[email protected]' };
    const createdUser = { id: 2, ...newUser };
    
    service.createUser(newUser).subscribe(user => {
      expect(user).toEqual(createdUser);
    });
    
    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('POST');
    expect(req.request.body).toEqual(newUser);
    req.flush(createdUser);
  });
});

Practice Exercise

Exercise: Build a Shopping Cart Service

Create a CartService that:
  1. Uses signals for reactive state
  2. Supports add, remove, update quantity
  3. Computes total price and item count
  4. Persists to localStorage
// cart.service.ts
interface CartItem {
  productId: number;
  name: string;
  price: number;
  quantity: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  private _items = signal<CartItem[]>(this.loadFromStorage());
  
  readonly items = this._items.asReadonly();
  readonly itemCount = computed(() => 
    this._items().reduce((sum, item) => sum + item.quantity, 0)
  );
  readonly total = computed(() =>
    this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  
  addItem(product: { id: number; name: string; price: number }) {
    this._items.update(items => {
      const existing = items.find(i => i.productId === product.id);
      if (existing) {
        return items.map(i => 
          i.productId === product.id 
            ? { ...i, quantity: i.quantity + 1 }
            : i
        );
      }
      return [...items, {
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: 1
      }];
    });
    this.saveToStorage();
  }
  
  removeItem(productId: number) {
    this._items.update(items => 
      items.filter(i => i.productId !== productId)
    );
    this.saveToStorage();
  }
  
  updateQuantity(productId: number, quantity: number) {
    if (quantity <= 0) {
      this.removeItem(productId);
      return;
    }
    this._items.update(items =>
      items.map(i => 
        i.productId === productId ? { ...i, quantity } : i
      )
    );
    this.saveToStorage();
  }
  
  clear() {
    this._items.set([]);
    this.saveToStorage();
  }
  
  private loadFromStorage(): CartItem[] {
    const data = localStorage.getItem('cart');
    return data ? JSON.parse(data) : [];
  }
  
  private saveToStorage() {
    localStorage.setItem('cart', JSON.stringify(this._items()));
  }
}

Summary

1

Services

Use @Injectable to create reusable, testable services
2

inject() Function

Modern way to inject dependencies, works everywhere
3

Injector Hierarchy

Services can be scoped to root, module, or component level
4

Providers

Configure with useClass, useValue, useFactory, or useExisting
5

InjectionTokens

Use for non-class dependencies and configuration

Next Steps

Next: Angular Signals

Deep dive into Angular’s reactive primitive for modern state management