Angular Services & Dependency Injection
Build reusable services, understand Angular's DI system, use inject(), hierarchical injectors, and structure your service layer for real production apps.
Services are where your business logic and data access live. Components should be thin — they display data and respond to user events. Services do the heavy lifting. Angular's dependency injection system wires everything together automatically.
Creating a Service
ng g service services/userimport { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root', // singleton — one instance for the whole app
})
export class UserService {
private users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
getAll(): User[] {
return this.users;
}
getById(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
add(user: Omit<User, 'id'>): User {
const newUser = { ...user, id: Date.now() };
this.users.push(newUser);
return newUser;
}
}
export interface User {
id: number;
name: string;
email: string;
}Injecting a Service
Constructor Injection (classic)
import { Component, OnInit } from '@angular/core';
import { UserService, User } from '../services/user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
@for (user of users; track user.id) {
<p>{{ user.name }}</p>
}
`,
})
export class UserListComponent implements OnInit {
users: User[] = [];
constructor(private userService: UserService) {}
ngOnInit(): void {
this.users = this.userService.getAll();
}
}inject() Function (modern — Angular 14+)
import { Component, OnInit } from '@angular/core';
import { inject } from '@angular/core';
import { UserService } from '../services/user.service';
@Component({ /* ... */ })
export class UserListComponent implements OnInit {
private userService = inject(UserService); // no constructor needed
users = this.userService.getAll(); // can call immediately
ngOnInit(): void {
// already populated above
}
}inject() is now the preferred approach in modern Angular. It works in components, directives, pipes, and other services. It cannot be called outside of an injection context (inside regular class methods or setTimeout, for example).
providedIn: 'root' — What It Means
providedIn: 'root' registers the service with the root injector. Angular creates one instance and shares it across the entire application. This is what you want for most services.
@Injectable({ providedIn: 'root' })
export class AuthService { /* ... */ }This is tree-shakeable — if no component injects AuthService, it won't be included in the production bundle.
Service Lifetimes in Angular
| Registration | Scope | Use for |
|---|---|---|
| providedIn: 'root' | Entire app (singleton) | Shared state, HTTP services, auth |
| providers: [MyService] in component | Component + children | Isolated service per component |
| providedIn: 'platform' | Multiple app instances | Shared between micro-frontends |
Component-scoped Service
When you provide a service in a component's providers, each instance of that component gets its own service instance — independent of other components:
@Component({
selector: 'app-shopping-cart',
standalone: true,
providers: [CartService], // each <app-shopping-cart> gets its own CartService
template: `...`,
})
export class ShoppingCartComponent {
private cart = inject(CartService);
}Services Calling Services
Services can inject other services — this is the normal pattern:
@Injectable({ providedIn: 'root' })
export class OrderService {
private http = inject(HttpClient);
private auth = inject(AuthService);
private toaster = inject(ToastService);
async placeOrder(order: CreateOrderRequest): Promise<Order> {
const token = this.auth.getToken();
const result = await firstValueFrom(
this.http.post<Order>('/api/orders', order, {
headers: { Authorization: `Bearer ${token}` },
})
);
this.toaster.success('Order placed!');
return result;
}
}Sharing State Between Components with a Service
Services are the simplest way to share state — inject the same singleton into multiple components:
@Injectable({ providedIn: 'root' })
export class CartService {
private items: CartItem[] = [];
add(product: Product, qty = 1): void {
const existing = this.items.find(i => i.productId === product.id);
if (existing) {
existing.quantity += qty;
} else {
this.items.push({ productId: product.id, name: product.name, quantity: qty, price: product.price });
}
}
remove(productId: number): void {
this.items = this.items.filter(i => i.productId !== productId);
}
getItems(): CartItem[] {
return [...this.items];
}
get total(): number {
return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
}
clear(): void {
this.items = [];
}
}Any component that injects CartService reads and modifies the same array — no prop-drilling needed.
For reactive state (components auto-updating when service state changes), use BehaviorSubject or Angular Signals:
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartService {
private _items = signal<CartItem[]>([]);
items = this._items.asReadonly();
total = computed(() => this._items().reduce((sum, i) => sum + i.price * i.quantity, 0));
count = computed(() => this._items().reduce((sum, i) => sum + i.quantity, 0));
add(item: CartItem): void {
this._items.update(items => [...items, item]);
}
clear(): void {
this._items.set([]);
}
}Components using signals automatically re-render when items or total change.
InjectionToken — Inject Non-Class Values
Sometimes you need to inject a config object, string, or factory — not a class. Use InjectionToken:
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
maxRetries: number;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');Provide it in app.config.ts:
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
{
provide: APP_CONFIG,
useValue: { apiUrl: 'https://api.myapp.com', maxRetries: 3 },
},
],
};Inject it anywhere:
@Injectable({ providedIn: 'root' })
export class ApiService {
private config = inject(APP_CONFIG);
getApiUrl(path: string): string {
return `${this.config.apiUrl}${path}`;
}
}Testing Services
Services with inject() are easy to unit test:
import { TestBed } from '@angular/core/testing';
import { CartService } from './cart.service';
describe('CartService', () => {
let service: CartService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CartService);
});
it('should add items and calculate total', () => {
service.add({ productId: 1, name: 'Widget', quantity: 2, price: 10 });
expect(service.getItems().length).toBe(1);
expect(service.total).toBe(20);
});
it('should clear the cart', () => {
service.add({ productId: 1, name: 'Widget', quantity: 1, price: 10 });
service.clear();
expect(service.getItems().length).toBe(0);
});
});Quick Reference
Create service: ng g s services/name
Singleton (app-wide): @Injectable({ providedIn: 'root' })
Inject (modern): private svc = inject(ServiceName)
Inject (classic): constructor(private svc: ServiceName) {}
Component-scoped: providers: [ServiceName] in @Component
Reactive state: signal() + computed() in service
Inject config: InjectionToken + provide: { provide: TOKEN, useValue }
Testing: TestBed.inject(ServiceName) Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.