On this page
Services and dependency injection
What is a service?
A service is a class that encapsulates reusable logic: data access, shared state, utilities, etc. It is the way to separate business logic from components.
Creating a service
Use the @Injectable decorator with providedIn: 'root' to create a singleton service:
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CounterService {
private readonly _value = signal(0);
readonly value = this._value.asReadonly();
increment(): void {
this._value.update(v => v + 1);
}
}With providedIn: 'root', Angular creates a single instance of the service for the entire application.
Injecting a service
Use the inject() function to get an instance of the service:
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-my-component',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Value: {{ counter.value() }}</p>
<button (click)="counter.increment()">+1</button>
`,
})
export class MyComponent {
readonly counter = inject(CounterService);
}Injector hierarchy
Angular has an injector hierarchy. Each level can provide its own instances:
Root Injector (providedIn: 'root')
└─ Component Injector (providers: [...])
└─ Child Component InjectorComponent-level service
If you need a unique instance per component (not a singleton), use providers:
@Component({
selector: 'app-form',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FormStateService], // New instance for each <app-form>
template: `...`,
})
export class FormComponent {
private readonly formState = inject(FormStateService);
}Pattern: private state, public reading
A fundamental pattern is keeping the mutable signal private and exposing a read-only version:
@Injectable({ providedIn: 'root' })
export class AuthService {
// Private: only the service can modify
private readonly _user = signal<User | null>(null);
// Public: components can only read
readonly user = this._user.asReadonly();
readonly isAuthenticated = computed(() => this._user() !== null);
login(credentials: Credentials): void {
// ... authentication logic
this._user.set(authenticatedUser);
}
logout(): void {
this._user.set(null);
}
}InjectionToken for values
To inject values that are not classes, use InjectionToken:
import { InjectionToken, inject } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
// Provide in the app config
export const appConfig = {
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' },
],
};
// Inject in a service
@Injectable({ providedIn: 'root' })
export class ApiService {
private readonly baseUrl = inject(API_URL);
}inject() in functions
inject() also works in functions that run in an injection context, such as guards and resolvers:
export function authGuard(): boolean {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) return true;
router.navigate(['/login']);
return false;
}Best practices
- One service, one responsibility — Don't create giant services
- Expose readonly — Use
.asReadonly()for public signals - inject() over constructor — More readable and flexible
- providedIn: 'root' — For most services
Practice
- Create a favorites service: Implement a
FavoritesServicewithprovidedIn: 'root', a private signal for the list, and methodsadd(),remove(), andisFavorite(). Expose the list asasReadonly(). - Inject in two components: Use
inject(FavoritesService)in two different components and verify they share the same instance (adding in one should be reflected in the other). - Create an InjectionToken: Define an
InjectionToken<string>for a base API URL, provide it in the app configuration, and use it in a service withinject().
In the next lesson, we will learn advanced routing: lazy loading, guards, resolvers, and nested layouts.
import { Injectable, inject, signal, computed } from '@angular/core';
// --- Model ---
interface Product {
id: number;
name: string;
price: number;
}
interface CartItem {
product: Product;
quantity: number;
}
// --- Singleton service (providedIn: 'root') ---
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly items = signal<CartItem[]>([]);
readonly cartItems = this.items.asReadonly();
readonly totalItems = computed(
() => this.items().reduce((sum, item) => sum + item.quantity, 0)
);
readonly totalPrice = computed(
() => this.items().reduce(
(sum, item) => sum + item.product.price * item.quantity, 0
)
);
add(product: Product): void {
this.items.update(list => {
const existing = list.find(i => i.product.id === product.id);
if (existing) {
return list.map(i =>
i.product.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...list, { product, quantity: 1 }];
});
}
remove(productId: number): void {
this.items.update(list =>
list.filter(i => i.product.id !== productId)
);
}
clear(): void {
this.items.set([]);
}
}
Sign in to track your progress