Services in Angular
A service in Angular is a class that encapsulates reusable business logic, data access, or shared state that does not belong to any single component. Services are the primary mechanism for sharing data and behavior between components that have no direct parent-child relationship.
The key principle: components should be thin. They handle display and user interactions. Services handle everything else — HTTP calls, caching, validation logic, and cross-component state.
Creating a Service
Use the Angular CLI to generate a service:
ng generate service services/user # or shorthand ng g s services/user
CREATE src/app/services/user.service.spec.ts CREATE src/app/services/user.service.ts
The generated service looks like this:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root', // register service with the root injector
})
export class UserService {
// your business logic goes here
}@Injectable decorator marks the class as a participant in Angular's dependency injection system. The providedIn: 'root' metadata registers it as a singleton available application-wide.A Real-World Service Example
Here is a complete UserService that encapsulates all user-related operations:
import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, tap, catchError, throwError } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
@Injectable({
providedIn: 'root',
})
export class UserService {
private readonly apiUrl = '/api/users';
// Shared reactive state using signals
private _currentUser = signal<User | null>(null);
readonly currentUser = this._currentUser.asReadonly();
constructor(private http: HttpClient) {}
getAll(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
create(data: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, data);
}
update(id: number, changes: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}`, changes);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
login(email: string, password: string): Observable<User> {
return this.http.post<User>('/api/auth/login', { email, password }).pipe(
tap(user => this._currentUser.set(user)),
catchError(err => throwError(() => new Error('Login failed: ' + err.message)))
);
}
logout(): void {
this._currentUser.set(null);
}
isAdmin(): boolean {
return this._currentUser()?.role === 'admin';
}
}Injecting Services into Components
Angular 14+ offers two equivalent ways to inject services. Both produce identical behavior:
// Method 1: Constructor injection (traditional)
@Component({ standalone: true, imports: [AsyncPipe] })
export class UserListComponent {
users$: Observable<User[]>;
constructor(private userService: UserService) {
this.users$ = this.userService.getAll();
}
}
// Method 2: inject() function (modern, preferred in Angular 14+)
import { inject } from '@angular/core';
@Component({ standalone: true, imports: [AsyncPipe] })
export class UserListComponent {
private userService = inject(UserService);
users$ = this.userService.getAll();
}inject() function is preferred in modern Angular because it works in constructors, field initializers, and also inside functions called from a component's constructor context — making it more flexible than constructor parameter injection.Service Lifetime and Scope
Where you provide a service determines when it is instantiated and how long it lives:
providedIn value | Scope | Instances |
|---|---|---|
'root' | Entire application | Single instance (singleton) |
'platform' | Multiple apps on the same page | One per platform |
'any' | Each lazy-loaded module gets its own | Multiple possible |
Component providers[] | Component and its children | One per component instance |
Module providers[] | Module and its components | One per module |
// Scoped to a specific component tree
@Component({
selector: 'app-checkout',
providers: [CartService], // new CartService instance for each CheckoutComponent
})
export class CheckoutComponent {
// This CartService instance is destroyed when CheckoutComponent is destroyed
private cart = inject(CartService);
}Sharing State Between Components
One of the most common uses of services is sharing state between sibling components. Using signals (Angular 16+):
// notification.service.ts
import { Injectable, signal, computed } from '@angular/core';
export interface Notification {
id: string;
message: string;
type: 'success' | 'error' | 'info';
}
@Injectable({ providedIn: 'root' })
export class NotificationService {
private _notifications = signal<Notification[]>([]);
// Public read-only access
readonly notifications = this._notifications.asReadonly();
readonly count = computed(() => this._notifications().length);
readonly hasErrors = computed(() =>
this._notifications().some(n => n.type === 'error')
);
add(message: string, type: Notification['type'] = 'info'): void {
const notification: Notification = {
id: crypto.randomUUID(),
message,
type,
};
this._notifications.update(current => [...current, notification]);
}
dismiss(id: string): void {
this._notifications.update(current => current.filter(n => n.id !== id));
}
clear(): void {
this._notifications.set([]);
}
}// Any component can inject and use the service
@Component({
selector: 'app-notification-bell',
standalone: true,
template: `
<button>
Notifications ({{ notifyService.count() }})
</button>
@for (n of notifyService.notifications(); track n.id) {
<div [class]="n.type">
{{ n.message }}
<button (click)="notifyService.dismiss(n.id)">X</button>
</div>
}
`,
})
export class NotificationBellComponent {
protected notifyService = inject(NotificationService);
}
// Another component triggers a notification
@Component({ selector: 'app-order-form' })
export class OrderFormComponent {
private notifications = inject(NotificationService);
submitOrder(): void {
this.orderService.submit(this.form.value).subscribe({
next: () => this.notifications.add('Order placed!', 'success'),
error: () => this.notifications.add('Order failed', 'error'),
});
}
}HTTP Service Pattern
Encapsulating HTTP calls in a service is a fundamental Angular pattern. The service becomes your data access layer:
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
export interface Product {
id: number;
name: string;
price: number;
category: string;
}
export interface ProductFilters {
category?: string;
minPrice?: number;
maxPrice?: number;
}
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly apiUrl = '/api/products';
// Cache the categories — they rarely change
categories$ = this.http.get<string[]>(`${this.apiUrl}/categories`).pipe(
shareReplay(1)
);
constructor(private http: HttpClient) {}
getAll(filters?: ProductFilters): Observable<Product[]> {
let params = new HttpParams();
if (filters?.category) params = params.set('category', filters.category);
if (filters?.minPrice) params = params.set('minPrice', String(filters.minPrice));
if (filters?.maxPrice) params = params.set('maxPrice', String(filters.maxPrice));
return this.http.get<Product[]>(this.apiUrl, { params });
}
getById(id: number): Observable<Product> {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}
getFeatured(): Observable<Product[]> {
return this.getAll().pipe(
map(products => products.filter(p => p.price > 100).slice(0, 6))
);
}
}Testing Services
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
UserService,
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should fetch all users', () => {
const mockUsers = [{ id: 1, name: 'Alice', email: 'a@b.com', role: 'user' as const }];
service.getAll().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});Best Practices
Use providedIn: 'root' for application-wide singletons — it enables tree-shaking (unused services are removed from the bundle)
Keep services focused: one service per domain concern (UserService, CartService, NotificationService)
Expose observable or signal properties for reactive state rather than plain mutable properties
Use the inject() function in Angular 14+ — it is more flexible than constructor injection
Never import services directly between feature modules — go through a shared service or facade
Write unit tests for services independently of components — they are easy to test in isolation
Summary
Services are the backbone of Angular application architecture:
- Decorated with
@Injectableto participate in dependency injection providedIn: 'root'creates a singleton — shared across the whole app- Can be scoped to a component or module for isolated state
- Use services to share state, encapsulate HTTP calls, and coordinate between components
- Inject via
inject()(modern) or constructor parameters (traditional)
Next: understand Dependency Injection — the system that makes services work.