Skip to main content

Angular Services Basics

Introduction

In Angular applications, services are a fundamental building block that help you organize and share code across your application. They are essentially TypeScript classes that can be injected into components and other services, providing a way to keep your component logic clean and focused on the user interface while moving business logic, data fetching, and other shared functionality into reusable service classes.

Services are central to Angular's dependency injection system, which is one of the framework's most powerful features. By the end of this tutorial, you'll understand what services are, why they're important, and how to create and use them in your Angular applications.

What Are Angular Services?

A service in Angular is a TypeScript class with a specific purpose. Services can:

  • Share data between components
  • Encapsulate business logic
  • Connect to external APIs and resources
  • Store application state
  • Provide utility functions

Unlike components, services don't have templates or views—they focus solely on functionality.

Why Use Services?

Services provide several important benefits:

  1. Reusability: Write code once and use it across multiple components
  2. Separation of concerns: Keep components focused on the view while business logic lives in services
  3. Maintainability: Centralize functionality to make updates easier
  4. Testability: Services can be tested independently from components
  5. Shared state: Maintain data that persists across component lifecycles

Creating Your First Service

Let's create a simple service that manages a list of tasks. We'll use the Angular CLI to generate our service:

bash
ng generate service services/Task

This command creates a task.service.ts file in a services folder with a basic service skeleton:

typescript
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 as a singleton instance.

Let's add some functionality to our service:

typescript
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 {
this.tasks.push({
id: this.nextId++,
title,
completed: false
});
}

deleteTask(id: number): void {
this.tasks = this.tasks.filter(task => task.id !== id);
}

toggleTaskCompletion(id: number): void {
const task = this.tasks.find(task => task.id === id);
if (task) {
task.completed = !task.completed;
}
}
}

Using Services in Components

Now that we have our TaskService, let's see how to use it in a component:

  1. First, generate a component:
bash
ng generate component TaskList
  1. Next, inject the TaskService into the component:
typescript
import { Component, OnInit } from '@angular/core';
import { TaskService } from '../services/task.service';

@Component({
selector: 'app-task-list',
templateUrl: './task-list.component.html',
styleUrls: ['./task-list.component.css']
})
export class TaskListComponent implements OnInit {
taskTitle = '';

constructor(public taskService: TaskService) { }

ngOnInit(): void {
// Initialize component with 2 sample tasks
this.taskService.addTask('Learn Angular Services');
this.taskService.addTask('Build a Todo Application');
}

addNewTask(): void {
if (this.taskTitle.trim()) {
this.taskService.addTask(this.taskTitle);
this.taskTitle = '';
}
}
}
  1. Create the component template:
html
<div class="task-container">
<h2>Task List</h2>

<div class="add-task">
<input [(ngModel)]="taskTitle" placeholder="Enter new task..." />
<button (click)="addNewTask()">Add Task</button>
</div>

<ul class="task-list">
<li *ngFor="let task of taskService.getTasks()" class="task-item">
<input
type="checkbox"
[checked]="task.completed"
(change)="taskService.toggleTaskCompletion(task.id)"
/>
<span [class.completed]="task.completed">{{ task.title }}</span>
<button (click)="taskService.deleteTask(task.id)">Delete</button>
</li>
</ul>
</div>
  1. Add some basic styling:
css
.task-container {
max-width: 500px;
margin: 0 auto;
}

.add-task {
margin-bottom: 20px;
display: flex;
gap: 10px;
}

.task-list {
list-style-type: none;
padding: 0;
}

.task-item {
padding: 8px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}

.completed {
text-decoration: line-through;
color: #888;
}

When the application runs, the TaskListComponent will inject the TaskService and use it to manage tasks. The component stays focused on the UI concerns while the service handles data management.

Understanding Dependency Injection

Angular's dependency injection system is what makes services so powerful. Here's how it works:

  1. You declare a dependency in a component's constructor
  2. The Angular injector finds or creates the requested service instance
  3. The service instance is injected into the component

The key benefit is that components don't need to know how services are created—they just use them. This enables looser coupling between classes and makes testing easier.

Service Providers and Scope

When creating a service, you have different options for how it's provided:

Root-level providers

typescript
@Injectable({
providedIn: 'root'
})

This creates a singleton instance available throughout your application. Most services should use this approach.

Module-level providers

typescript
@NgModule({
providers: [TaskService]
})

This creates a singleton instance within a specific NgModule.

Component-level providers

typescript
@Component({
selector: 'app-task-list',
templateUrl: './task-list.component.html',
providers: [TaskService]
})

This creates a new instance for each instance of the component and its child components.

Real-World Example: Data Service with HTTP

Let's create a more practical example—a service that fetches data from an API:

typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

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) { }

getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}

getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}

createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}

updateUser(id: number, changes: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}`, changes);
}

deleteUser(id: number): Observable<unknown> {
return this.http.delete(`${this.apiUrl}/${id}`);
}
}

And here's how you might use this service in a component:

typescript
import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';

@Component({
selector: 'app-user-list',
template: `
<div *ngIf="loading">Loading users...</div>
<div *ngIf="error">{{ error }}</div>
<ul *ngIf="users.length">
<li *ngFor="let user of users">
{{ user.name }} ({{ user.email }})
</li>
</ul>
`
})
export class UserListComponent implements OnInit {
users: any[] = [];
loading = false;
error = '';

constructor(private userService: UserService) { }

ngOnInit(): void {
this.loading = true;
this.userService.getUsers().subscribe({
next: (data) => {
this.users = data;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load users: ' + err.message;
this.loading = false;
}
});
}
}

This example demonstrates how a service can encapsulate all API interaction logic, keeping the component clean and focused on displaying the data.

Service Communication

Services can also be used to facilitate communication between unrelated components:

typescript
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class MessageService {
// Subject acts as both an Observable and an Observer
private messageSource = new Subject<string>();

// Expose the Subject as an Observable for subscribers
message$ = this.messageSource.asObservable();

// Method to send messages
sendMessage(message: string): void {
this.messageSource.next(message);
}
}

Then any component can send messages:

typescript
import { Component } from '@angular/core';
import { MessageService } from '../services/message.service';

@Component({
selector: 'app-sender',
template: `
<input #messageInput placeholder="Type message" />
<button (click)="sendMessage(messageInput.value)">Send</button>
`
})
export class SenderComponent {
constructor(private messageService: MessageService) { }

sendMessage(message: string): void {
this.messageService.sendMessage(message);
}
}

And any other component can receive those messages:

typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { MessageService } from '../services/message.service';
import { Subscription } from 'rxjs';

@Component({
selector: 'app-receiver',
template: `
<div>Last message received: {{ lastMessage || 'None' }}</div>
`
})
export class ReceiverComponent implements OnInit, OnDestroy {
lastMessage = '';
private subscription: Subscription | undefined;

constructor(private messageService: MessageService) { }

ngOnInit(): void {
this.subscription = this.messageService.message$.subscribe(
message => this.lastMessage = message
);
}

ngOnDestroy(): void {
// Always unsubscribe to prevent memory leaks
this.subscription?.unsubscribe();
}
}

This pattern allows completely unrelated components to communicate without direct references to each other.

Summary

Angular services are a powerful way to organize your application code:

  • Services are singleton objects that provide functionality across your application
  • Dependency Injection makes services easy to use and test
  • Services help maintain separation of concerns by keeping business logic out of components
  • Services are perfect for data fetching, state management, and cross-component communication

By leveraging services effectively, you'll write more maintainable, testable, and scalable Angular applications.

Additional Resources

Exercises

  1. Create a CounterService that keeps track of a count value and provides methods to increment, decrement, and reset the counter.

  2. Build a LocalStorageService that wraps browser's localStorage API and provides methods to get, set, and remove items with proper typing.

  3. Create a ThemeService that allows components to switch between light and dark themes, broadcasting theme changes to all subscribed components.

  4. Extend the TaskService to persist tasks to localStorage so they survive page refreshes.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)