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 value | Scope | When to use |
|---|---|---|
'root' | Application-wide singleton | Default; most services (HTTP, auth, state) |
'platform' | Shared across multiple Angular apps on the same page | Rare; micro-frontend setups |
'any' | One instance per lazy-loaded module | Feature-module isolation (deprecated in Angular 16) |
| A module class | Singleton within that module | Module-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
- Beyond string and number: TypeScript Literal Types
- Mastering Variable Declarations in TypeScript
- Mastering TypeScript Union Types
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.