What Are Services?
Services are classes that encapsulate reusable logic — data fetching, business rules, state management, logging, and more. They keep your components focused on the view while services handle everything else.
Why Use Services?
- Separation of concerns — components handle the UI, services handle logic
- Code reuse — multiple components share the same service
- Testability — services are easy to unit test
- Singleton pattern — one instance shared across the entire application
Creating a Service
Using the CLI
bash
ng generate service services/user
# Shorthand: ng g s services/userManual Creation
typescript
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class UserService {
private users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
];
getUsers() {
return this.users;
}
getUserById(id: number) {
return this.users.find(user => user.id === id);
}
addUser(user: { name: string; email: string }) {
const newUser = { ...user, id: this.users.length + 1 };
this.users.push(newUser);
return newUser;
}
}The @Injectable Decorator
typescript
@Injectable({
providedIn: 'root', // Available application-wide as a singleton
})providedIn Value | Scope |
|---|---|
'root' | Application-wide singleton (most common) |
'platform' | Shared across multiple apps on the page |
'any' | Unique instance per lazy-loaded module |
Using a Service in a Component
With inject() Function (Modern)
typescript
import { Component, inject, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
<h2>Users</h2>
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} ({{ user.email }})</li>
}
</ul>
`,
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users: { id: number; name: string; email: string }[] = [];
ngOnInit() {
this.users = this.userService.getUsers();
}
}With Constructor Injection (Classic)
typescript
@Component({
selector: 'app-user-list',
standalone: true,
template: `<!-- same as above -->`,
})
export class UserListComponent implements OnInit {
users: { id: number; name: string; email: string }[] = [];
constructor(private userService: UserService) {}
ngOnInit() {
this.users = this.userService.getUsers();
}
}Recommendation: Use the inject() function for new code — it works in components, directives, pipes, guards, and resolver functions.
Practical Service Examples
Logger Service
typescript
@Injectable({ providedIn: 'root' })
export class LoggerService {
private logs: string[] = [];
log(message: string) {
const timestamp = new Date().toISOString();
const entry = `[${timestamp}] ${message}`;
this.logs.push(entry);
console.log(entry);
}
warn(message: string) {
console.warn(`⚠️ ${message}`);
}
error(message: string) {
console.error(`❌ ${message}`);
}
getLogs(): string[] {
return [...this.logs];
}
}Shopping Cart Service
typescript
interface CartItem {
productId: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private items: CartItem[] = [];
getItems(): CartItem[] {
return [...this.items];
}
addItem(product: { id: number; name: string; price: number }) {
const existing = this.items.find(i => i.productId === product.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({
productId: product.id,
name: product.name,
price: product.price,
quantity: 1,
});
}
}
removeItem(productId: number) {
this.items = this.items.filter(i => i.productId !== productId);
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
getItemCount(): number {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
clear() {
this.items = [];
}
}Notification Service
typescript
export interface Notification {
id: number;
message: string;
type: 'success' | 'error' | 'info' | 'warning';
timestamp: Date;
}
@Injectable({ providedIn: 'root' })
export class NotificationService {
private notifications: Notification[] = [];
private nextId = 1;
show(message: string, type: Notification['type'] = 'info') {
const notification: Notification = {
id: this.nextId++,
message,
type,
timestamp: new Date(),
};
this.notifications.push(notification);
// Auto-dismiss after 5 seconds
setTimeout(() => this.dismiss(notification.id), 5000);
}
success(message: string) { this.show(message, 'success'); }
error(message: string) { this.show(message, 'error'); }
warning(message: string) { this.show(message, 'warning'); }
dismiss(id: number) {
this.notifications = this.notifications.filter(n => n.id !== id);
}
getAll(): Notification[] {
return [...this.notifications];
}
}Service-to-Service Communication
Services can inject other services:
typescript
@Injectable({ providedIn: 'root' })
export class OrderService {
private cartService = inject(CartService);
private notificationService = inject(NotificationService);
private loggerService = inject(LoggerService);
placeOrder() {
const items = this.cartService.getItems();
if (items.length === 0) {
this.notificationService.warning('Cart is empty!');
return;
}
const total = this.cartService.getTotal();
this.loggerService.log(`Order placed: ${items.length} items, $${total}`);
this.cartService.clear();
this.notificationService.success('Order placed successfully!');
}
}Services with Observables
For reactive data, services often expose Observables using BehaviorSubject:
typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ThemeService {
private themeSubject = new BehaviorSubject<'light' | 'dark'>('light');
theme$: Observable<'light' | 'dark'> = this.themeSubject.asObservable();
toggleTheme() {
const current = this.themeSubject.value;
this.themeSubject.next(current === 'light' ? 'dark' : 'light');
}
setTheme(theme: 'light' | 'dark') {
this.themeSubject.next(theme);
}
}typescript
// In a component
export class AppComponent {
private themeService = inject(ThemeService);
theme$ = this.themeService.theme$;
toggleTheme() {
this.themeService.toggleTheme();
}
}html
<div [class]="(theme$ | async) === 'dark' ? 'dark-mode' : 'light-mode'">
<button (click)="toggleTheme()">Toggle Theme</button>
</div>Best Practices
- Use
providedIn: 'root'for most services — creates a tree-shakable singleton - Keep services focused — one service per concern (auth, cart, logging)
- Return new arrays/objects for immutability (
return [...this.items]) - Use interfaces for data models, not the service class
- Prefer
inject()over constructor injection for cleaner code
Next Steps
Services need to be made available to components through dependency injection — Angular's powerful DI system. Let's explore how it works under the hood.