Angular Custom Services
Introduction
In Angular applications, services are a fundamental building block that help you organize and share code across your application. Unlike components, which are focused on the user interface, services are dedicated to implementing business logic, data fetching, logging, or any functionality that isn't directly related to views.
Custom services allow you to:
- Organize your code into cohesive, focused units
- Share data and functionality across components
- Implement the single responsibility principle
- Make your code more testable and maintainable
In this tutorial, we'll explore how to create custom services in Angular, inject them into components, and use them effectively in your applications.
What are Angular Services?
Services in Angular are singleton objects (by default) that get instantiated only once during the lifetime of an application. They provide methods that maintain data throughout the life of an application, thus avoiding the need to pass data between components.
Creating Your First Custom Service
Let's create a simple data service that will store and retrieve a list of tasks.
First, you can generate a service using the Angular CLI:
ng generate service services/task
This command creates a new service called task.service.ts
in a 'services' folder. Here's what the generated file looks like:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TaskService {
constructor() { }
}
The @Injectable()
decorator marks the class as one that participates in the dependency injection system. The providedIn: 'root'
option makes the service available throughout the application.
Adding Functionality to Your Service
Let's enhance our service by adding methods to manage tasks:
import { Injectable } from '@angular/core';
interface Task {
id: number;
title: string;
completed: boolean;
}
@Injectable({
providedIn: 'root'
})
export class TaskService {
private tasks: Task[] = [];
private nextId = 1;
constructor() { }
getTasks(): Task[] {
return this.tasks;
}
addTask(title: string): void {
const task: Task = {
id: this.nextId++,
title,
completed: false
};
this.tasks.push(task);
}
deleteTask(id: number): void {
const index = this.tasks.findIndex(task => task.id === id);
if (index !== -1) {
this.tasks.splice(index, 1);
}
}
toggleTaskCompletion(id: number): void {
const task = this.tasks.find(task => task.id === id);
if (task) {
task.completed = !task.completed;
}
}
}
Now we have a service that can:
- Store a list of tasks
- Add new tasks
- Delete tasks
- Toggle task completion status
Using Services in Components
To use our service in a component, we need to inject it. Here's how to use our TaskService in a TaskListComponent:
import { Component, OnInit } from '@angular/core';
import { TaskService } from '../services/task.service';
@Component({
selector: 'app-task-list',
template: `
<div>
<h2>Task List</h2>
<div>
<input #taskInput placeholder="Add a new task">
<button (click)="addTask(taskInput.value); taskInput.value=''">Add Task</button>
</div>
<ul>
<li *ngFor="let task of tasks">
<input type="checkbox"
[checked]="task.completed"
(change)="toggleCompletion(task.id)">
<span [class.completed]="task.completed">{{ task.title }}</span>
<button (click)="deleteTask(task.id)">Delete</button>
</li>
</ul>
</div>
`,
styles: [`
.completed {
text-decoration: line-through;
color: gray;
}
`]
})
export class TaskListComponent implements OnInit {
tasks: any[] = [];
constructor(private taskService: TaskService) { }
ngOnInit(): void {
// Load tasks when component initializes
this.tasks = this.taskService.getTasks();
}
addTask(title: string): void {
if (title.trim()) {
this.taskService.addTask(title);
}
}
deleteTask(id: number): void {
this.taskService.deleteTask(id);
}
toggleCompletion(id: number): void {
this.taskService.toggleTaskCompletion(id);
}
}
In this component:
- We inject the TaskService in the constructor
- We use the service's methods to manage tasks
- We display the tasks in the template and provide UI for interacting with them
Creating Services with Dependencies
Services can also have their own dependencies. Let's create a more complex example with a logging service that our task service will use:
First, let's create a LoggingService:
ng generate service services/logging
And implement it:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggingService {
constructor() { }
log(message: string): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${message}`);
}
error(message: string): void {
const timestamp = new Date().toISOString();
console.error(`[${timestamp}] ERROR: ${message}`);
}
}
Now, let's update our TaskService to use the LoggingService:
import { Injectable } from '@angular/core';
import { LoggingService } from './logging.service';
interface Task {
id: number;
title: string;
completed: boolean;
}
@Injectable({
providedIn: 'root'
})
export class TaskService {
private tasks: Task[] = [];
private nextId = 1;
// Inject the LoggingService
constructor(private loggingService: LoggingService) { }
getTasks(): Task[] {
this.loggingService.log('Retrieved all tasks');
return this.tasks;
}
addTask(title: string): void {
const task: Task = {
id: this.nextId++,
title,
completed: false
};
this.tasks.push(task);
this.loggingService.log(`Added new task: "${title}"`);
}
deleteTask(id: number): void {
const index = this.tasks.findIndex(task => task.id === id);
if (index !== -1) {
const taskTitle = this.tasks[index].title;
this.tasks.splice(index, 1);
this.loggingService.log(`Deleted task: "${taskTitle}"`);
} else {
this.loggingService.error(`Task with id ${id} not found`);
}
}
toggleTaskCompletion(id: number): void {
const task = this.tasks.find(task => task.id === id);
if (task) {
task.completed = !task.completed;
const status = task.completed ? 'completed' : 'active';
this.loggingService.log(`Task "${task.title}" marked as ${status}`);
} else {
this.loggingService.error(`Task with id ${id} not found`);
}
}
}
In this updated service:
- We inject the LoggingService in the constructor
- We use it to log information about what's happening in our TaskService
Real-world Example: HTTP Service
Let's create a more practical service that fetches data from an API using Angular's HttpClient:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { LoggingService } from './logging.service';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
constructor(
private http: HttpClient,
private loggingService: LoggingService
) { }
getUsers(): Observable<User[]> {
this.loggingService.log('Fetching users from API...');
return this.http.get<User[]>(this.apiUrl).pipe(
tap(users => this.loggingService.log(`Fetched ${users.length} users`)),
catchError(error => {
this.loggingService.error(`Failed to fetch users: ${error.message}`);
throw error;
})
);
}
getUserById(id: number): Observable<User> {
const url = `${this.apiUrl}/${id}`;
this.loggingService.log(`Fetching user with ID: ${id}`);
return this.http.get<User>(url).pipe(
tap(user => this.loggingService.log(`Fetched user: ${user.name}`)),
catchError(error => {
this.loggingService.error(`Failed to fetch user: ${error.message}`);
throw error;
})
);
}
}
Using this service in a component:
import { Component, OnInit } from '@angular/core';
import { User, UserService } from '../services/user.service';
@Component({
selector: 'app-user-list',
template: `
<div>
<h2>User List</h2>
<div *ngIf="loading">Loading...</div>
<div *ngIf="error">{{ error }}</div>
<ul>
<li *ngFor="let user of users">
{{ user.name }} ({{ user.email }})
</li>
</ul>
</div>
`
})
export class UserListComponent implements OnInit {
users: User[] = [];
loading = false;
error: string | null = null;
constructor(private userService: UserService) { }
ngOnInit(): void {
this.loading = true;
this.userService.getUsers().subscribe({
next: (users) => {
this.users = users;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load users. Please try again later.';
this.loading = false;
console.error(err);
}
});
}
}
Service Providers and Scope
By default with providedIn: 'root'
, services are singleton instances available application-wide. However, you can change the scope of a service by configuring providers differently:
Component-level Service
If you want a service to be scoped to a component and its children:
@Component({
selector: 'app-feature',
templateUrl: './feature.component.html',
providers: [FeatureService]
})
export class FeatureComponent { }
This creates a new instance of FeatureService for this component and its children.
Module-level Service
For a service to be available throughout a feature module:
@NgModule({
declarations: [...],
imports: [...],
providers: [FeatureService]
})
export class FeatureModule { }
Best Practices for Custom Services
- Single Responsibility: Each service should have a single responsibility
- Naming Convention: Use the suffix 'Service' in the service class name
- Inject Services Only Where Needed: Don't inject services unless the component truly needs them
- Use Interfaces: Define interfaces for your service data structures
- Error Handling: Implement proper error handling in services
- Documentation: Add JSDoc comments to describe service methods and parameters
Summary
In this tutorial, you've learned:
- How to create custom services in Angular
- How to inject services into components
- How services can depend on other services
- How to work with HTTP in services
- How to configure service scope and providers
- Best practices for implementing services
Custom services are a powerful tool in Angular development that help you organize your code and separate concerns. By moving business logic and data access out of components and into services, you create more maintainable, testable, and reusable code.
Exercises
To practice working with services:
- Create a shopping cart service that allows adding, removing, and listing items
- Build a data persistence service that saves data to localStorage
- Implement an authentication service with login/logout functionality
- Create a service that uses WebSockets to receive real-time updates
- Build a theming service that allows changing your application's visual theme
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)