Services & Dependency Injection

Dependency Injection in Angular

Understand Angular's dependency injection system — providers, injection tokens, hierarchical injectors, and advanced DI patterns.

What is Dependency Injection?

Dependency Injection (DI) is a design pattern where a class receives its dependencies from an external source rather than creating them itself. Angular has a built-in DI framework that is central to how the framework works.

Without DI (Tightly Coupled)

typescript
// ❌ Bad: Class creates its own dependency
class OrderService {
  private http = new HttpClient(); // Hard to test, hard to swap

  getOrders() {
    return this.http.get('/api/orders');
  }
}

With DI (Loosely Coupled)

typescript
// ✅ Good: Dependency is injected
@Injectable({ providedIn: 'root' })
class OrderService {
  private http = inject(HttpClient); // Angular provides it

  getOrders() {
    return this.http.get('/api/orders');
  }
}

How Angular's DI Works

Angular's DI system has three key concepts:

1. Provider     → "Here's how to create this dependency"
2. Injector     → "I manage dependencies and their lifecycle"
3. Consumer     → "I need this dependency"

The Flow

@Injectable({ providedIn: 'root' })
class MyService { }                 ← PROVIDER declares it

@Component(...)
class MyComponent {
  service = inject(MyService);      ← CONSUMER requests it
}
                                    ↕
                              INJECTOR resolves it

Providing Services

Application-Wide (Most Common)

typescript
@Injectable({
  providedIn: 'root', // Singleton for the entire app
})
export class AuthService { }

In Application Config

typescript
// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    // Custom providers
    { provide: API_URL, useValue: 'https://api.example.com' },
  ],
};

In a Component (Scoped Instance)

typescript
@Component({
  selector: 'app-editor',
  standalone: true,
  providers: [EditorStateService], // New instance for this component tree
  template: `...`,
})
export class EditorComponent { }

When provided in a component, each component instance gets its own service instance.

Injection Methods

inject() Function (Preferred)

typescript
import { inject } from '@angular/core';

@Component({ /* ... */ })
export class DashboardComponent {
  private userService = inject(UserService);
  private router = inject(Router);
  private http = inject(HttpClient);
}

Constructor Injection (Classic)

typescript
@Component({ /* ... */ })
export class DashboardComponent {
  constructor(
    private userService: UserService,
    private router: Router,
    private http: HttpClient,
  ) {}
}

Provider Types

Angular supports several ways to define providers:

useClass — Provide a Class

typescript
providers: [
  { provide: LoggerService, useClass: LoggerService },
  // Shorthand (equivalent):
  LoggerService,
]

Swap implementations:

typescript
providers: [
  { provide: LoggerService, useClass: ConsoleLoggerService },
  // Whenever LoggerService is requested, provide ConsoleLoggerService
]

useValue — Provide a Value

typescript
import { InjectionToken } from '@angular/core';

export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');

// In providers
providers: [
  { provide: API_BASE_URL, useValue: 'https://api.bigxstar.com' },
  { provide: APP_CONFIG, useValue: { debug: false, theme: 'dark' } },
]

// In a service
export class ApiService {
  private baseUrl = inject(API_BASE_URL);
}

useFactory — Provide via Factory Function

typescript
export const IS_BROWSER = new InjectionToken<boolean>('IS_BROWSER');

providers: [
  {
    provide: IS_BROWSER,
    useFactory: () => typeof window !== 'undefined',
  },
]

Factory with dependencies:

typescript
providers: [
  {
    provide: UserService,
    useFactory: (http: HttpClient, config: AppConfig) => {
      return new UserService(http, config.apiUrl);
    },
    deps: [HttpClient, APP_CONFIG],
  },
]

useExisting — Alias a Provider

typescript
providers: [
  ConsoleLoggerService,
  { provide: LoggerService, useExisting: ConsoleLoggerService },
  // Both tokens resolve to the same ConsoleLoggerService instance
]

Injection Tokens

When injecting non-class values (strings, numbers, objects, interfaces), use InjectionToken:

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

export interface AppSettings {
  apiUrl: string;
  debug: boolean;
  maxRetries: number;
}

export const APP_SETTINGS = new InjectionToken<AppSettings>('APP_SETTINGS');
export const MAX_FILE_SIZE = new InjectionToken<number>('MAX_FILE_SIZE');
typescript
// app.config.ts
providers: [
  {
    provide: APP_SETTINGS,
    useValue: {
      apiUrl: 'https://api.bigxstar.com',
      debug: false,
      maxRetries: 3,
    },
  },
  { provide: MAX_FILE_SIZE, useValue: 10 * 1024 * 1024 }, // 10MB
]
typescript
// usage
export class UploadService {
  private settings = inject(APP_SETTINGS);
  private maxSize = inject(MAX_FILE_SIZE);
}

Hierarchical Injector

Angular's DI system is hierarchical — there are multiple injector levels:

Root Injector (Application)
  └── Component Injector (Parent)
       └── Component Injector (Child)
            └── Component Injector (Grandchild)

When a component requests a dependency:

  1. Angular checks the component's own injector
  2. If not found, moves up to the parent component's injector
  3. Continues up until reaching the root injector
  4. If still not found, throws an error

Optional Dependencies

typescript
export class MyComponent {
  // Returns null if not found (instead of throwing)
  private analytics = inject(AnalyticsService, { optional: true });

  trackEvent(name: string) {
    this.analytics?.track(name);
  }
}

Self and SkipSelf

typescript
export class ChildComponent {
  // Only look in THIS component's injector
  private service = inject(MyService, { self: true });

  // Skip THIS injector, start looking from parent
  private parentService = inject(MyService, { skipSelf: true });
}

Practical Example: Environment-Based Configuration

typescript
// environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000',
};

// injection token
export const ENVIRONMENT = new InjectionToken('ENVIRONMENT');

// app.config.ts
import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: ENVIRONMENT, useValue: environment },
    provideHttpClient(),
    provideRouter(routes),
  ],
};

// api.service.ts
@Injectable({ providedIn: 'root' })
export class ApiService {
  private env = inject(ENVIRONMENT);
  private http = inject(HttpClient);

  get<T>(endpoint: string) {
    return this.http.get<T>(`${this.env.apiUrl}${endpoint}`);
  }
}

Next Steps

With services and dependency injection mastered, you're ready to use one of Angular's most important built-in services — the HttpClient for making API calls and fetching data.