Angular Services and Dependency Injection: A Complete Guide

Angular’s dependency injection (DI) system is one of the framework’s defining features. Rather than creating service instances manually, you declare what a component or another service needs, and Angular’s injector provides the right instance automatically. This makes your code modular, testable, and free from tight coupling. In this guide you will learn how to create services, register them with the DI system, understand injection scope, and write tests that swap real services for mocks.

What Is a Service in Angular?

A service is a TypeScript class that encapsulates logic or data that should be shared across multiple components. Common examples include HTTP communication, authentication state, logging, and local-storage management. Services keep components thin — components handle only template interaction, while services handle everything else.

Creating a Service with the Angular CLI

# Generates src/app/core/product.service.ts + product.service.spec.ts
ng generate service core/product
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

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

@Injectable({
  providedIn: 'root'   // singleton registered at the root injector
})
export class ProductService {

  private readonly apiUrl = 'https://api.example.com/products';

  constructor(private http: HttpClient) {}   // DI: Angular injects HttpClient

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

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

  create(product: Omit<Product, 'id'>): Observable<Product> {
    return this.http.post<Product>(this.apiUrl, product);
  }

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

Injecting a Service into a Component

import { Component, OnInit } from '@angular/core';
import { ProductService, Product } from '../core/product.service';

@Component({
  selector: 'app-product-list',
  template: `
    <h2>Products</h2>
    <ul>
      <li *ngFor="let p of products">
        {{ p.name }} — ${{ p.price }}
      </li>
    </ul>
    <p *ngIf="error" class="error">{{ error }}</p>
  `
})
export class ProductListComponent implements OnInit {

  products: Product[] = [];
  error = '';

  // Angular injects ProductService via the constructor
  constructor(private productService: ProductService) {}

  ngOnInit(): void {
    this.productService.getAll().subscribe({
      next:  products => this.products = products,
      error: err      => this.error = 'Failed to load products'
    });
  }
}

inject() Function (Angular 14+)

Angular 14 introduced the inject() function as an alternative to constructor injection. It works inside injection contexts (constructors, field initialisers, factory functions):

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

@Component({ selector: 'app-dashboard', template: '...' })
export class DashboardComponent implements OnInit {

  // inject() is cleaner for multiple services and works in standalone components
  private productService = inject(ProductService);
  private router         = inject(Router);

  ngOnInit(): void {
    this.productService.getAll().subscribe(...);
  }
}

Injection Scope: providedIn Options

providedIn valueScopeWhen to use
'root'Application-wide singletonDefault; most services (HTTP, auth, state)
'platform'Shared across multiple Angular apps on the same pageRare; micro-frontend setups
'any'One instance per lazy-loaded moduleFeature-module isolation (deprecated in Angular 16)
A module classSingleton within that moduleModule-scoped services

Component-Level Providers (Non-Singleton)

Register a service in the component’s providers array to get a fresh instance scoped to that component tree (useful for forms, state machines, or wizard steps):

@Component({
  selector: 'app-checkout',
  templateUrl: './checkout.component.html',
  providers: [CheckoutStateService]   // new instance per CheckoutComponent
})
export class CheckoutComponent {
  constructor(private state: CheckoutStateService) {}
  // Each instance of CheckoutComponent gets its own CheckoutStateService
}

InjectionToken — Injecting Non-Class Values

Use InjectionToken to inject plain values (configuration objects, feature flags, strings) that have no class type:

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

export interface AppConfig {
  apiUrl:    string;
  featureX:  boolean;
}

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

// Register in AppModule or bootstrapApplication providers
{
  provide:  APP_CONFIG,
  useValue: { apiUrl: 'https://api.prod.com', featureX: true }
}

// Inject in a service or component
@Injectable({ providedIn: 'root' })
export class FeatureService {
  constructor(@Inject(APP_CONFIG) private config: AppConfig) {}

  isFeatureXEnabled(): boolean {
    return this.config.featureX;
  }
}

Unit Testing with a Mock Service

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of, throwError }           from 'rxjs';
import { ProductListComponent }     from './product-list.component';
import { ProductService }           from '../core/product.service';

describe('ProductListComponent', () => {
  let fixture: ComponentFixture<ProductListComponent>;
  let mockService: jasmine.SpyObj<ProductService>;

  beforeEach(() => {
    // Create a spy object that replaces the real ProductService
    mockService = jasmine.createSpyObj('ProductService', ['getAll']);

    TestBed.configureTestingModule({
      declarations: [ProductListComponent],
      providers: [
        { provide: ProductService, useValue: mockService }  // inject mock
      ]
    });

    fixture = TestBed.createComponent(ProductListComponent);
  });

  it('should display products on success', () => {
    mockService.getAll.and.returnValue(
      of([{ id: 1, name: 'Laptop', price: 999 }])
    );
    fixture.detectChanges();
    const li = fixture.nativeElement.querySelectorAll('li');
    expect(li.length).toBe(1);
    expect(li[0].textContent).toContain('Laptop');
  });

  it('should show error on failure', () => {
    mockService.getAll.and.returnValue(throwError(() => new Error('500')));
    fixture.detectChanges();
    const err = fixture.nativeElement.querySelector('.error');
    expect(err.textContent).toContain('Failed to load');
  });
});

See Also

Conclusion

Angular’s dependency injection system is the glue that holds an application together. Register services with providedIn: 'root' for application-wide singletons, use component-level providers for scoped instances, and reach for InjectionToken when injecting plain values or configuration. The modern inject() function makes constructor injection boilerplate optional. Always test by providing a mock service via TestBed rather than using the real service — this keeps unit tests fast, deterministic, and independent of network calls.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.