Services are singleton objects that provide specific functionality to your Angular application. They encapsulate business logic, data access, and other operations that can be shared across components.
What are Services?
Services are TypeScript classes that:
Contain reusable business logic
Manage application state
Handle data access (HTTP requests, local storage)
Provide utilities and helper functions
Enable communication between components
Services promote the single responsibility principle by separating business logic from presentation logic in components.
Creating a Service
Create a service using the @Injectable decorator:
import { Injectable } from '@angular/core' ;
@ Injectable ({
providedIn: 'root'
})
export class ProductService {
private products : Product [] = [];
getProducts () : Product [] {
return this . products ;
}
addProduct ( product : Product ) : void {
this . products . push ( product );
}
getProductById ( id : number ) : Product | undefined {
return this . products . find ( p => p . id === id );
}
deleteProduct ( id : number ) : void {
this . products = this . products . filter ( p => p . id !== id );
}
}
interface Product {
id : number ;
name : string ;
price : number ;
}
Using Services in Components
Inject services using the inject() function:
import { Component , inject } from '@angular/core' ;
import { ProductService } from './product.service' ;
@ Component ({
selector: 'product-list' ,
standalone: true ,
template: `
<h2>Products</h2>
<ul>
@for (product of products; track product.id) {
<li>{{ product.name }} - {{ product.price | currency }}</li>
}
</ul>
<button (click)="addNewProduct()">Add Product</button>
`
})
export class ProductListComponent {
private productService = inject ( ProductService );
products = this . productService . getProducts ();
addNewProduct () {
this . productService . addProduct ({
id: Date . now (),
name: 'New Product' ,
price: 99.99
});
this . products = this . productService . getProducts ();
}
}
HTTP Data Service
Services commonly handle HTTP requests:
import { Injectable , inject } from '@angular/core' ;
import { HttpClient } from '@angular/common/http' ;
import { Observable , catchError , map , of } from 'rxjs' ;
interface User {
id : number ;
name : string ;
email : string ;
}
@ Injectable ({
providedIn: 'root'
})
export class UserService {
private http = inject ( HttpClient );
private apiUrl = 'https://api.example.com/users' ;
getUsers () : Observable < User []> {
return this . http . get < User []>( this . apiUrl ). pipe (
catchError ( this . handleError < User []>( 'getUsers' , []))
);
}
getUser ( id : number ) : Observable < User | undefined > {
const url = ` ${ this . apiUrl } / ${ id } ` ;
return this . http . get < User >( url ). pipe (
catchError ( this . handleError < User >( 'getUser' ))
);
}
createUser ( user : Omit < User , 'id' >) : Observable < User > {
return this . http . post < User >( this . apiUrl , user ). pipe (
catchError ( this . handleError < User >( 'createUser' ))
);
}
updateUser ( user : User ) : Observable < User > {
const url = ` ${ this . apiUrl } / ${ user . id } ` ;
return this . http . put < User >( url , user ). pipe (
catchError ( this . handleError < User >( 'updateUser' ))
);
}
deleteUser ( id : number ) : Observable < void > {
const url = ` ${ this . apiUrl } / ${ id } ` ;
return this . http . delete < void >( url ). pipe (
catchError ( this . handleError < void >( 'deleteUser' ))
);
}
private handleError < T >( operation = 'operation' , result ?: T ) {
return ( error : any ) : Observable < T > => {
console . error ( ` ${ operation } failed:` , error );
return of ( result as T );
};
}
}
Using the HTTP service:
import { Component , inject , OnInit } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { UserService } from './user.service' ;
@ Component ({
selector: 'user-list' ,
standalone: true ,
imports: [ CommonModule ],
template: `
@if (loading) {
<p>Loading users...</p>
} @else if (error) {
<p class="error">{{ error }}</p>
} @else {
<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 : User [] = [];
loading = false ;
error : string | null = null ;
ngOnInit () {
this . loadUsers ();
}
loadUsers () {
this . loading = true ;
this . userService . getUsers (). subscribe ({
next : ( users ) => {
this . users = users ;
this . loading = false ;
},
error : ( err ) => {
this . error = 'Failed to load users' ;
this . loading = false ;
}
});
}
}
Always unsubscribe from observables or use the async pipe to prevent memory leaks.
State Management Service
Services can manage application state:
import { Injectable , signal , computed } from '@angular/core' ;
interface CartItem {
id : number ;
name : string ;
price : number ;
quantity : number ;
}
@ Injectable ({
providedIn: 'root'
})
export class CartService {
// Using signals for reactive state
private items = signal < CartItem []>([]);
// Computed values
readonly itemCount = computed (() =>
this . items (). reduce (( sum , item ) => sum + item . quantity , 0 )
);
readonly total = computed (() =>
this . items (). reduce (( sum , item ) => sum + ( item . price * item . quantity ), 0 )
);
readonly cartItems = this . items . asReadonly ();
addItem ( item : Omit < CartItem , 'quantity' >) {
const currentItems = this . items ();
const existingItem = currentItems . find ( i => i . id === item . id );
if ( existingItem ) {
this . items . set (
currentItems . map ( i =>
i . id === item . id
? { ... i , quantity: i . quantity + 1 }
: i
)
);
} else {
this . items . set ([ ... currentItems , { ... item , quantity: 1 }]);
}
}
removeItem ( id : number ) {
this . items . set ( this . items (). filter ( item => item . id !== id ));
}
updateQuantity ( id : number , quantity : number ) {
if ( quantity <= 0 ) {
this . removeItem ( id );
return ;
}
this . items . set (
this . items (). map ( item =>
item . id === id ? { ... item , quantity } : item
)
);
}
clear () {
this . items . set ([]);
}
}
Using the state management service:
import { Component , inject } from '@angular/core' ;
import { CartService } from './cart.service' ;
@ Component ({
selector: 'shopping-cart' ,
standalone: true ,
template: `
<h2>Shopping Cart</h2>
<p>Items: {{ cartService.itemCount() }}</p>
<p>Total: {{ cartService.total() | currency }}</p>
@for (item of cartService.cartItems(); track item.id) {
<div class="cart-item">
<span>{{ item.name }}</span>
<input
type="number"
[value]="item.quantity"
(change)="updateQuantity(item.id, $event)">
<span>{{ item.price * item.quantity | currency }}</span>
<button (click)="cartService.removeItem(item.id)">Remove</button>
</div>
}
<button (click)="cartService.clear()">Clear Cart</button>
`
})
export class ShoppingCartComponent {
cartService = inject ( CartService );
updateQuantity ( id : number , event : Event ) {
const input = event . target as HTMLInputElement ;
const quantity = parseInt ( input . value , 10 );
this . cartService . updateQuantity ( id , quantity );
}
}
Logger Service
Utility service for logging:
import { Injectable } from '@angular/core' ;
export enum LogLevel {
Debug = 0 ,
Info = 1 ,
Warn = 2 ,
Error = 3
}
@ Injectable ({
providedIn: 'root'
})
export class LoggerService {
private logLevel = LogLevel . Debug ;
setLogLevel ( level : LogLevel ) {
this . logLevel = level ;
}
debug ( message : string , ... args : any []) {
if ( this . logLevel <= LogLevel . Debug ) {
console . debug ( `[DEBUG] ${ message } ` , ... args );
}
}
info ( message : string , ... args : any []) {
if ( this . logLevel <= LogLevel . Info ) {
console . info ( `[INFO] ${ message } ` , ... args );
}
}
warn ( message : string , ... args : any []) {
if ( this . logLevel <= LogLevel . Warn ) {
console . warn ( `[WARN] ${ message } ` , ... args );
}
}
error ( message : string , error ?: any ) {
if ( this . logLevel <= LogLevel . Error ) {
console . error ( `[ERROR] ${ message } ` , error );
}
}
}
Communication Between Components
Use services to share data between unrelated components:
import { Injectable , signal } from '@angular/core' ;
@ Injectable ({
providedIn: 'root'
})
export class MessageService {
private messageSignal = signal < string >( '' );
readonly message = this . messageSignal . asReadonly ();
sendMessage ( message : string ) {
this . messageSignal . set ( message );
}
clearMessage () {
this . messageSignal . set ( '' );
}
}
// Component A - sends message
@ Component ({
selector: 'message-sender' ,
standalone: true ,
template: `
<input #input type="text">
<button (click)="send(input.value)">Send</button>
`
})
export class MessageSenderComponent {
private messageService = inject ( MessageService );
send ( message : string ) {
this . messageService . sendMessage ( message );
}
}
// Component B - receives message
@ Component ({
selector: 'message-receiver' ,
standalone: true ,
template: `
<p>Received: {{ messageService.message() }}</p>
`
})
export class MessageReceiverComponent {
messageService = inject ( MessageService );
}
Service with Dependencies
Services can depend on other services:
import { Injectable , inject } from '@angular/core' ;
import { HttpClient } from '@angular/common/http' ;
import { LoggerService } from './logger.service' ;
import { Observable , tap , catchError } from 'rxjs' ;
@ Injectable ({
providedIn: 'root'
})
export class ApiService {
private http = inject ( HttpClient );
private logger = inject ( LoggerService );
private baseUrl = 'https://api.example.com' ;
getData < T >( endpoint : string ) : Observable < T > {
this . logger . info ( `Fetching data from ${ endpoint } ` );
return this . http . get < T >( ` ${ this . baseUrl } / ${ endpoint } ` ). pipe (
tap ( data => this . logger . debug ( 'Data received' , data )),
catchError ( error => {
this . logger . error ( `Failed to fetch ${ endpoint } ` , error );
throw error ;
})
);
}
}
Best Practices
Single Responsibility Each service should have one clear purpose
Use providedIn: 'root' For application-wide singletons
Return Observables For asynchronous operations
Use Signals For reactive state management
Keep services focused - One responsibility per service
Use signals for state - Reactive and performant
Handle errors gracefully - Provide user-friendly error messages
Document public APIs - Make services easy to understand
Write unit tests - Services are easy to test in isolation
Avoid circular dependencies - Structure services hierarchically
Next Steps
Dependency Injection Deep dive into Angular’s DI system
HTTP Client Learn about making HTTP requests