When developing Angular applications, one of the most important concepts to understand is Services.
They form the foundation for building scalable, maintainable, and testable applications.
Angular services are used to share data and logic across multiple components, centralize communication with back-end APIs, and organize reusable business logic that does not belong to any specific component.
This article provides an in-depth explanation of what services are, why they are essential, and how to implement them effectively in Angular applications.
Table of Contents
- Introduction to Angular Services
- Why Services Are Important
- Core Benefits of Using Services
- Creating and Registering a Service
- Understanding Dependency Injection in Angular
- Sharing Data Between Components with Services
- Using Services for API Communication
- Services for Business Logic and State Management
- Injectable Decorator Explained
- Providing Services in Different Scopes
- Singleton Services and ProvidedIn Options
- Using Services in Lazy-Loaded Modules
- Handling Observables in Services
- Real-World Example: User Service
- Real-World Example: Product Service
- Unit Testing Angular Services
- Common Mistakes When Using Services
- Best Practices for Angular Services
- When Not to Use Services
- Conclusion
1. Introduction to Angular Services
In Angular, a service is a class that contains reusable logic, such as fetching data from an API, manipulating data, or performing operations that are not directly tied to a component’s view.
A service class is typically decorated with the @Injectable()
decorator, allowing Angular’s dependency injection (DI) system to manage its lifecycle and dependencies.
Services help separate the business logic from the presentation layer, resulting in a cleaner, modular structure.
2. Why Services Are Important
Without services, you might end up duplicating logic across multiple components.
For instance, if multiple components need access to user data, each might make its own HTTP call — leading to redundant code and inconsistencies.
By using a service, you can:
- Centralize data and logic
- Share state between components
- Simplify testing
- Improve maintainability
- Encourage reusability
In short, services act as the bridge between your user interface (components) and your application logic.
3. Core Benefits of Using Services
Angular services allow you to:
- Share data and logic between components
- Centralize API calls and avoid code duplication
- Improve code maintainability by separating concerns
- Enhance testability using dependency injection
Each of these benefits contributes to a scalable and clean architecture, particularly in large projects.
4. Creating and Registering a Service
Creating a service in Angular is straightforward.
You can generate it using the Angular CLI:
ng generate service user
This command creates two files:
user.service.ts
user.service.spec.ts
Example:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() { }
getUser() {
return { name: 'John Doe', email: '[email protected]' };
}
}
Using the Service in a Component
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-profile',
template: `
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
`
})
export class ProfileComponent implements OnInit {
user: any;
constructor(private userService: UserService) {}
ngOnInit() {
this.user = this.userService.getUser();
}
}
Here, the service provides data to the component without embedding logic directly inside it.
5. Understanding Dependency Injection in Angular
Angular’s dependency injection (DI) system is what makes services so powerful.
DI allows you to declare dependencies in your components or other services, and Angular automatically provides instances of those dependencies.
In the above example, the UserService
is injected into the ProfileComponent
constructor.
Angular creates and manages the UserService
instance, ensuring that all components share the same instance when provided in the root injector.
6. Sharing Data Between Components with Services
One of the most common uses of services is to share data between unrelated components.
Example: Data Sharing Service
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private messageSource = new BehaviorSubject<string>('Initial Message');
currentMessage = this.messageSource.asObservable();
changeMessage(message: string) {
this.messageSource.next(message);
}
}
Component A (Sender)
import { Component } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-component-a',
template: `
<button (click)="sendMessage()">Send Message</button>
`
})
export class ComponentA {
constructor(private dataService: DataService) {}
sendMessage() {
this.dataService.changeMessage('Hello from Component A');
}
}
Component B (Receiver)
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-component-b',
template: `
<p>Message: {{ message }}</p>
`
})
export class ComponentB implements OnInit {
message!: string;
constructor(private dataService: DataService) {}
ngOnInit() {
this.dataService.currentMessage.subscribe(msg => this.message = msg);
}
}
Here, both components share a single instance of DataService
.
When ComponentA
updates the message, ComponentB
receives the change instantly.
7. Using Services for API Communication
Another major reason to use services is to handle HTTP requests.
Example: API Service Using HttpClient
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<any> {
return this.http.get(this.apiUrl);
}
getUserById(id: number): Observable<any> {
return this.http.get(${this.apiUrl}/${id}
);
}
}
Using the Service in a Component
import { Component, OnInit } from '@angular/core';
import { ApiService } from './api.service';
@Component({
selector: 'app-user-list',
template: `
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`
})
export class UserListComponent implements OnInit {
users: any[] = [];
constructor(private apiService: ApiService) {}
ngOnInit() {
this.apiService.getUsers().subscribe(data => this.users = data);
}
}
By isolating HTTP logic in a service, you make your code more maintainable and easier to test.
8. Services for Business Logic and State Management
Services are not just for HTTP calls; they can also handle business logic and application state.
Example — A service managing cart items in an e-commerce app:
@Injectable({
providedIn: 'root'
})
export class CartService {
private items: any[] = [];
addItem(item: any) {
this.items.push(item);
}
removeItem(id: number) {
this.items = this.items.filter(i => i.id !== id);
}
getItems() {
return this.items;
}
clearCart() {
this.items = [];
}
}
This service encapsulates the cart’s logic, and multiple components can access or modify it as needed.
9. Injectable Decorator Explained
The @Injectable()
decorator marks a class as available for dependency injection.
It tells Angular that the class might depend on other services and allows Angular to create an instance of it.
@Injectable({
providedIn: 'root'
})
export class ExampleService { }
The providedIn
property defines where the service should be available — globally (root) or in a specific module.
10. Providing Services in Different Scopes
You can register services in multiple ways:
providedIn: 'root'
: Singleton instance shared across the entire app.providedIn: 'any'
: A new instance for each lazy-loaded module.- Registering in the module’s
providers
array.
Example:
@NgModule({
providers: [MyCustomService]
})
export class FeatureModule {}
11. Singleton Services and ProvidedIn Options
When you use providedIn: 'root'
, Angular creates a single instance of that service for the entire application.
This is useful when sharing global state, like user authentication or configuration data.
However, if you want each feature module to have its own service instance, you can use:
@Injectable({
providedIn: 'any'
})
export class FeatureService {}
12. Using Services in Lazy-Loaded Modules
When using lazy-loaded modules, you might want different instances of the same service for each module.
By setting providedIn: 'any'
, Angular provides unique instances for each lazy-loaded module — helping with modularity and isolation.
13. Handling Observables in Services
Since Angular uses RxJS, services often return observables for data streams.
Example:
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notificationSubject = new BehaviorSubject<string>('');
getNotifications() {
return this.notificationSubject.asObservable();
}
sendNotification(message: string) {
this.notificationSubject.next(message);
}
}
Components can subscribe to notifications to react in real time.
14. Real-World Example: User Service
@Injectable({
providedIn: 'root'
})
export class UserService {
private users: any[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
getAll() {
return this.users;
}
getUser(id: number) {
return this.users.find(u => u.id === id);
}
addUser(user: any) {
this.users.push(user);
}
}
This service can be shared across multiple components — user profile, admin panel, dashboard, etc.
15. Real-World Example: Product Service
@Injectable({
providedIn: 'root'
})
export class ProductService {
private products = [
{ id: 1, name: 'Laptop', price: 1200 },
{ id: 2, name: 'Phone', price: 800 }
];
getAllProducts() {
return this.products;
}
getProductById(id: number) {
return this.products.find(p => p.id === id);
}
addProduct(product: any) {
this.products.push(product);
}
}
It centralizes all product-related operations, keeping your component lightweight.
16. Unit Testing Angular Services
Since services are independent of the view layer, they are easy to test.
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UserService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return users', () => {
const users = service.getAll();
expect(users.length).toBeGreaterThan(0);
});
});
Testing services ensures your business logic remains correct and reliable.
17. Common Mistakes When Using Services
- Forgetting to add
@Injectable()
decorator. - Providing the service multiple times unnecessarily.
- Mixing UI logic with business logic inside services.
- Creating circular dependencies between services.
- Overusing services for trivial tasks that belong to components.
18. Best Practices for Angular Services
- Use services for shared logic, not for UI manipulation.
- Keep services stateless unless they manage specific application state.
- Return observables instead of plain values for asynchronous data.
- Prefer
providedIn: 'root'
for global services. - Follow Single Responsibility Principle (SRP) — one service, one responsibility.
- Write unit tests for all critical services.
19. When Not to Use Services
Avoid creating a service when:
- The logic belongs only to a single component.
- The service duplicates another existing one.
- The data can be managed locally in a component without reuse.
Leave a Reply