How to Use Angular Services

How to Use Angular Services Angular services are one of the most powerful and foundational concepts in modern Angular development. They provide a structured, reusable, and testable way to encapsulate business logic, data handling, and application-wide functionality. Unlike components, which are primarily responsible for rendering UI and handling user interactions, services focus on delivering spec

Oct 30, 2025 - 13:29
Oct 30, 2025 - 13:29
 0

How to Use Angular Services

Angular services are one of the most powerful and foundational concepts in modern Angular development. They provide a structured, reusable, and testable way to encapsulate business logic, data handling, and application-wide functionality. Unlike components, which are primarily responsible for rendering UI and handling user interactions, services focus on delivering specific capabilities—such as fetching data from an API, managing application state, or logging events—that can be shared across multiple components.

Understanding how to use Angular services effectively is critical for building scalable, maintainable, and performant applications. Whether you're developing a simple single-page application or a complex enterprise system, services help you adhere to the Single Responsibility Principle, reduce code duplication, and improve testability. In this comprehensive guide, we’ll walk you through everything you need to know—from creating your first service to implementing advanced patterns and best practices—so you can leverage services to their full potential.

Step-by-Step Guide

Creating a Service in Angular

To begin using services in Angular, you first need to generate one. Angular CLI provides a streamlined command to create services automatically with the correct structure and decorators.

Open your terminal in the root directory of your Angular project and run:

ng generate service services/data

This command creates two files:

  • data.service.ts – the TypeScript class definition
  • data.service.spec.ts – the unit test file (optional but recommended)

The generated service looks like this:

import { Injectable } from '@angular/core';

@Injectable({

providedIn: 'root'

})

export class DataService {

constructor() { }

}

The @Injectable() decorator is essential. It tells Angular that this class can be injected as a dependency into other classes—such as components, directives, or other services. The providedIn: 'root' option registers the service at the root injector level, making it a singleton available throughout the entire application. This is the most common and recommended approach for most services.

Adding Logic to a Service

Now that you have a service, you can add methods and properties to encapsulate functionality. Let’s create a service that fetches user data from a REST API.

Update your data.service.ts file:

import { Injectable } from '@angular/core';

import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';

export interface User {

id: number;

name: string;

email: string;

}

@Injectable({

providedIn: 'root'

})

export class DataService {

private apiUrl = 'https://jsonplaceholder.typicode.com/users';

constructor(private http: HttpClient) { }

getUsers(): Observable {

return this.http.get(this.apiUrl);

}

getUserById(id: number): Observable {

return this.http.get(${this.apiUrl}/${id});

}

createUser(user: Omit): Observable {

return this.http.post(this.apiUrl, user);

}

updateUser(id: number, user: Partial): Observable {

return this.http.put(${this.apiUrl}/${id}, user);

}

deleteUser(id: number): Observable {

return this.http.delete(${this.apiUrl}/${id});

}

}

In this example, we’ve:

  • Imported HttpClient to make HTTP requests
  • Defined an interface User for type safety
  • Created methods for CRUD operations
  • Injected HttpClient via the constructor

Notice how the service doesn’t handle UI logic. It simply provides a clean API for data operations. This separation of concerns is key to Angular’s architecture.

Injecting the Service into a Component

Now that the service is ready, you need to use it inside a component. Let’s create a component that displays a list of users.

Generate the component:

ng generate component user-list

In user-list.component.ts:

import { Component, OnInit } from '@angular/core';

import { DataService, User } from '../services/data.service';

import { Observable } from 'rxjs';

@Component({

selector: 'app-user-list',

templateUrl: './user-list.component.html',

styleUrls: ['./user-list.component.css']

})

export class UserListComponent implements OnInit {

users$: Observable | undefined;

constructor(private dataService: DataService) { }

ngOnInit(): void {

this.users$ = this.dataService.getUsers();

}

}

In the template user-list.component.html:

<div *ngIf="users$ | async as users; else loading">

<ul>

<li *ngFor="let user of users">

<strong>{{ user.name }}</strong> — {{ user.email }}

</li>

</ul>

</div> <ng-template

loading>

<p>Loading users...</p>

</ng-template>

Key points:

  • We inject DataService into the component’s constructor
  • We assign the observable returned by getUsers() to a property users$ (the $ suffix is a convention to indicate an Observable)
  • We use the async pipe in the template to automatically subscribe and unsubscribe, preventing memory leaks

Using Services for Shared State

Services are excellent for managing shared application state. Unlike components, which are destroyed and recreated, services remain active for the lifetime of the application (when provided in root). This makes them ideal for storing user preferences, authentication tokens, or cart items.

Let’s create a AuthService that manages user login state:

import { Injectable } from '@angular/core';

import { BehaviorSubject } from 'rxjs';

export interface User {

id: number;

name: string;

token: string;

}

@Injectable({

providedIn: 'root'

})

export class AuthService {

private currentUserSubject = new BehaviorSubject(null);

public currentUser$ = this.currentUserSubject.asObservable();

constructor() {

// Load user from localStorage on initialization

const savedUser = localStorage.getItem('currentUser');

if (savedUser) {

this.currentUserSubject.next(JSON.parse(savedUser));

}

}

login(user: User): void {

this.currentUserSubject.next(user);

localStorage.setItem('currentUser', JSON.stringify(user));

}

logout(): void {

this.currentUserSubject.next(null);

localStorage.removeItem('currentUser');

}

isLoggedIn(): boolean {

return this.currentUserSubject.value !== null;

}

getCurrentUser(): User | null {

return this.currentUserSubject.value;

}

}

Now, any component can subscribe to currentUser$ to react to login/logout events:

import { Component, OnInit } from '@angular/core';

import { AuthService, User } from '../services/auth.service';

@Component({

selector: 'app-header',

template:

<nav>

<span *ngIf="currentUser; else loginLink">

Welcome, {{ currentUser.name }}!

<button (click)="logout()">Logout</button>

</span> <ng-template

loginLink>

<a routerLink="/login">Login</a>

</ng-template>

</nav>

})

export class HeaderComponent implements OnInit {

currentUser: User | null = null;

constructor(private authService: AuthService) { }

ngOnInit(): void {

this.authService.currentUser$.subscribe(user => {

this.currentUser = user;

});

}

logout(): void {

this.authService.logout();

}

}

This pattern ensures that the login state is synchronized across all components without requiring direct communication between them.

Dependency Injection and Tree-Shaking

Angular’s dependency injection system is highly optimized. When you use providedIn: 'root', Angular registers the service at the application root level and includes it in the main bundle only if it’s actually used. This enables tree-shaking—removing unused code during the build process—which reduces your final bundle size.

Alternatively, you can provide services at the component level:

@Component({

selector: 'app-user-detail',

templateUrl: './user-detail.component.html',

providers: [DataService] // <-- New instance per component

})

export class UserDetailComponent { }

When you provide a service at the component level, Angular creates a new instance for that component and its children. This is useful when you need isolated state—for example, a form component that manages its own temporary data without affecting other instances.

However, avoid overusing component-level providers unless necessary. Root-level providers are preferred for shared functionality because they’re more efficient and predictable.

Using Services with RxJS for Complex Data Flows

Services often work with RxJS observables to handle asynchronous data streams. This is especially important for real-time applications, such as chat systems, live dashboards, or notifications.

Let’s extend our DataService to include a WebSocket-based real-time feed:

import { Injectable } from '@angular/core';

import { Observable, Subject, fromEvent } from 'rxjs';

import { WebSocketSubject } from 'rxjs/webSocket';

@Injectable({

providedIn: 'root'

})

export class RealTimeService {

private socket$: WebSocketSubject<any>;

constructor() {

this.socket$ = new WebSocketSubject('wss://realtime.example.com/data');

}

getRealTimeUpdates(): Observable<any> {

return this.socket$;

}

sendMessage(message: any): void {

this.socket$.next(message);

}

close(): void {

this.socket$.complete();

}

}

Then, in a component:

import { Component, OnInit, OnDestroy } from '@angular/core';

import { RealTimeService } from '../services/real-time.service';

import { Subscription } from 'rxjs';

@Component({

selector: 'app-real-time-feed',

template:

<div *ngFor="let item of updates">

{{ item.message }}

</div>

})

export class RealTimeFeedComponent implements OnInit, OnDestroy {

updates: any[] = [];

private subscription: Subscription = new Subscription();

constructor(private realTimeService: RealTimeService) { }

ngOnInit(): void {

this.subscription.add(

this.realTimeService.getRealTimeUpdates().subscribe(data => {

this.updates.push(data);

})

);

}

ngOnDestroy(): void {

this.subscription.unsubscribe();

this.realTimeService.close();

}

}

Using Subscription ensures proper cleanup. Always unsubscribe from observables in components to prevent memory leaks—especially when using services that emit continuous streams.

Best Practices

1. Use Singleton Services for Shared Logic

Always provide services at the root level unless you have a specific reason to create multiple instances. Root-provided services are singletons, meaning there’s only one instance across the entire application. This ensures consistent state and efficient resource usage.

2. Keep Services Focused and Single-Purpose

Follow the Single Responsibility Principle. A service should do one thing well. Avoid creating “god services” that handle authentication, data fetching, logging, and configuration. Instead, create separate services:

  • AuthService – handles login, logout, token management
  • ApiService – manages HTTP requests and interceptors
  • LoggerService – logs events to console or remote server
  • StorageService – wraps localStorage/sessionStorage

This modular approach makes services easier to test, maintain, and reuse.

3. Use Interfaces for Type Safety

Always define TypeScript interfaces for the data your services return or accept. This improves code readability, enables IntelliSense, and catches errors at compile time.

Example:

export interface Product {

id: number;

name: string;

price: number;

category: string;

}

Then use it in your service methods:

getProducts(): Observable<Product[]> { ... }

4. Handle Errors Gracefully

HTTP requests and asynchronous operations can fail. Always handle errors in your services using RxJS operators like catchError.

import { catchError } from 'rxjs/operators';

import { of } from 'rxjs';

getUsers(): Observable<User[]> {

return this.http.get<User[]>(this.apiUrl).pipe(

catchError(error => {

console.error('Failed to fetch users:', error);

return of([]); // Return empty array as fallback

})

);

}

This prevents your application from crashing and provides a better user experience.

5. Avoid Direct DOM Manipulation in Services

Services should never manipulate the DOM directly. That’s the responsibility of components and directives. If you need to show notifications, use a NotificationService that emits events, and let a dedicated component (like a toast bar) handle the visual display.

6. Use RxJS Subjects for State Management

For managing application state (like user preferences, theme settings, or cart items), use BehaviorSubject or ReplaySubject instead of plain variables. These allow components to subscribe and receive the latest value immediately upon subscription.

7. Separate Data Access from Business Logic

Don’t mix API calls with business rules. Create a ApiService to handle HTTP communication and a UserService to handle user-related logic (e.g., validating email format, calculating user roles).

This separation allows you to swap out the data layer (e.g., from REST to GraphQL) without changing business logic.

8. Write Unit Tests for Services

Services are ideal for unit testing because they’re independent of the UI. Use Angular’s testing utilities to mock dependencies.

import { TestBed } from '@angular/core/testing';

import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import { DataService } from './data.service';

describe('DataService', () => {

let service: DataService;

let httpMock: HttpTestingController;

beforeEach(() => {

TestBed.configureTestingModule({

imports: [HttpClientTestingModule],

providers: [DataService]

});

service = TestBed.inject(DataService);

httpMock = TestBed.inject(HttpTestingController);

});

it('should fetch users', () => {

const mockUsers = [{ id: 1, name: 'John', email: 'john@example.com' }];

service.getUsers().subscribe(users => {

expect(users).toEqual(mockUsers);

});

const req = httpMock.expectOne('https://jsonplaceholder.typicode.com/users');

expect(req.request.method).toBe('GET');

req.flush(mockUsers);

});

afterEach(() => {

httpMock.verify();

});

});

Testing services ensures your application logic remains robust during refactoring.

9. Use Interceptors for Cross-Cutting Concerns

Instead of duplicating headers, error handling, or token injection in every service, use HTTP interceptors.

import { Injectable } from '@angular/core';

import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';

import { Observable } from 'rxjs';

import { AuthService } from './auth.service';

@Injectable()

export class AuthInterceptor implements HttpInterceptor {

constructor(private authService: AuthService) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

const token = this.authService.getToken();

if (token) {

req = req.clone({

setHeaders: {

Authorization: Bearer ${token}

}

});

}

return next.handle(req);

}

}

Register it in your module:

providers: [

{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }

]

Interceptors keep your services clean and ensure consistent behavior across all HTTP calls.

10. Avoid Circular Dependencies

Circular dependencies occur when Service A depends on Service B, and Service B depends on Service A. This can cause runtime errors and break the dependency injection system.

Solutions:

  • Refactor to extract shared logic into a third service
  • Use lazy injection with Injector
  • Use events or observables instead of direct method calls

Tools and Resources

Core Angular Tools

  • Angular CLI – The official command-line interface for generating services, components, and modules. Use ng generate service to scaffold services quickly.
  • Angular DevTools – A browser extension for Chrome and Firefox that allows you to inspect services, components, and dependency injection trees in real time.
  • RxJS DevTools – Helps visualize and debug observable streams, especially useful when working with complex data flows in services.

Testing Frameworks

  • Jasmine – The default testing framework for Angular. Used to write unit tests for services.
  • Karma – The test runner that executes tests in real browsers.
  • Testing Library for Angular – Encourages testing behavior over implementation details, making tests more maintainable.

Third-Party Libraries

  • NgRx – A state management library built on RxJS. Use it for complex applications where services alone aren’t sufficient to manage global state.
  • NGXS – A simpler alternative to NgRx, using classes and decorators for state management. Great for teams new to reactive state.
  • Angular Material – While primarily UI-focused, its components often integrate with services for data binding and form handling.
  • Superstruct – A runtime type validation library that can be used inside services to validate incoming data before processing.

Documentation and Learning Resources

  • Angular.io – The official documentation. The “Dependency Injection” and “Services and Dependency Injection” sections are essential reading.
  • Angular University – Offers in-depth video courses on services, RxJS, and state management.
  • ReactiveX.io – The definitive resource for understanding RxJS operators and patterns used extensively in services.
  • Stack Overflow – Search for tags like angular-services and angular-dependency-injection to find real-world solutions to common problems.
  • GitHub Repositories – Study open-source Angular applications on GitHub to see how professional teams structure services.

Code Editors and Extensions

  • Visual Studio Code – The most popular editor for Angular development. Install the Angular Language Service extension for autocomplete, error detection, and template validation.
  • Prettier + ESLint – Ensure consistent code formatting and catch potential bugs in service logic.
  • Angular Snippets – A collection of code snippets for generating services, components, and pipes quickly.

Real Examples

Example 1: Cart Service for an E-Commerce App

Imagine building an online store. The shopping cart needs to persist across pages, allow multiple users to add/remove items, and calculate totals.

import { Injectable } from '@angular/core';

import { BehaviorSubject } from 'rxjs';

export interface CartItem {

productId: number;

name: string;

price: number;

quantity: number;

}

@Injectable({

providedIn: 'root'

})

export class CartService {

private cartSubject = new BehaviorSubject<CartItem[]>([]);

public cart$ = this.cartSubject.asObservable();

constructor() {

const savedCart = localStorage.getItem('cart');

if (savedCart) {

this.cartSubject.next(JSON.parse(savedCart));

}

}

addItem(item: CartItem): void {

const currentCart = this.cartSubject.value;

const existingItem = currentCart.find(i => i.productId === item.productId);

if (existingItem) {

existingItem.quantity += item.quantity;

} else {

currentCart.push(item);

}

this.cartSubject.next([...currentCart]);

this.saveToStorage();

}

removeItem(productId: number): void {

const currentCart = this.cartSubject.value.filter(i => i.productId !== productId);

this.cartSubject.next([...currentCart]);

this.saveToStorage();

}

getTotalItems(): number {

return this.cartSubject.value.reduce((sum, item) => sum + item.quantity, 0);

}

getTotalPrice(): number {

return this.cartSubject.value.reduce((sum, item) => sum + (item.price * item.quantity), 0);

}

clear(): void {

this.cartSubject.next([]);

this.saveToStorage();

}

private saveToStorage(): void {

localStorage.setItem('cart', JSON.stringify(this.cartSubject.value));

}

}

This service is used in multiple components:

  • ProductCardComponent – Adds items to cart
  • CartSidebarComponent – Displays current items and total
  • CheckoutComponent – Retrieves cart data for order submission

No component needs to know how the cart is stored or calculated. They simply interact with the service’s API.

Example 2: Notification Service

Many applications need to show alerts, success messages, or warnings. Instead of hardcoding these in components, create a reusable notification service.

import { Injectable } from '@angular/core';

import { BehaviorSubject } from 'rxjs';

export interface Notification {

id: string;

message: string;

type: 'success' | 'error' | 'warning' | 'info';

duration?: number;

}

@Injectable({

providedIn: 'root'

})

export class NotificationService {

private notificationsSubject = new BehaviorSubject<Notification[]>([]);

public notifications$ = this.notificationsSubject.asObservable();

add(message: string, type: 'success' | 'error' | 'warning' | 'info', duration = 5000): void {

const id = Date.now().toString();

const notification: Notification = { id, message, type, duration };

const current = this.notificationsSubject.value;

this.notificationsSubject.next([...current, notification]);

setTimeout(() => {

this.remove(id);

}, duration);

}

remove(id: string): void {

const current = this.notificationsSubject.value.filter(n => n.id !== id);

this.notificationsSubject.next([...current]);

}

clear(): void {

this.notificationsSubject.next([]);

}

}

Use it anywhere:

constructor(private notificationService: NotificationService) {}

onSubmit() {

this.apiService.saveData().subscribe({

next: () => this.notificationService.add('Saved successfully!', 'success'),

error: () => this.notificationService.add('Failed to save.', 'error')

});

}

And display notifications in a dedicated component:

<div *ngFor="let notify of notifications$ | async" [ngClass]="notify.type">

{{ notify.message }}

<button (click)="notificationService.remove(notify.id)">✖</button>

</div>

Example 3: Configuration Service

Applications often need to load environment-specific settings (e.g., API endpoints, feature flags).

import { Injectable } from '@angular/core';

export interface AppConfig {

apiUrl: string;

enableAnalytics: boolean;

defaultLanguage: string;

}

@Injectable({

providedIn: 'root'

})

export class ConfigService {

private config: AppConfig = {

apiUrl: 'https://api.example.com',

enableAnalytics: true,

defaultLanguage: 'en'

};

constructor() {

// Load from environment file or localStorage if needed

const envConfig = window['appConfig'] || {};

this.config = { ...this.config, ...envConfig };

}

get(key: keyof AppConfig): any {

return this.config[key];

}

getAll(): AppConfig {

return { ...this.config };

}

update(config: Partial<AppConfig>): void {

this.config = { ...this.config, ...config };

}

}

Use in components:

constructor(private config: ConfigService) {}

ngOnInit() {

const apiUrl = this.config.get('apiUrl');

// Use apiUrl to initialize HTTP clients

}

FAQs

What is the difference between a service and a component in Angular?

Components are responsible for rendering UI and handling user interactions. They have templates, styles, and lifecycle hooks. Services, on the other hand, are plain TypeScript classes that encapsulate logic—like data fetching, authentication, or utility functions—and are designed to be shared across components. Components use services; services do not use components.

Do I need to provide a service in every module?

No. If you use providedIn: 'root', the service is automatically registered at the application root level and available everywhere. You only need to provide it in a module if you want to create a scoped instance (e.g., for lazy-loaded modules or isolated components).

Can a service inject another service?

Yes. Services can inject other services through their constructors. This is common—for example, a UserService might inject an ApiService to make HTTP requests. Angular’s dependency injection system handles the chain automatically.

How do I test a service that uses HttpClient?

Use Angular’s HttpClientTestingModule and HttpTestingController to mock HTTP requests. You can simulate responses, verify request URLs and methods, and ensure error handling works correctly—all without making actual network calls.

What happens if I forget to unsubscribe from an observable in a service?

Services themselves are singletons and live for the lifetime of the app, so unsubscribing from observables inside services is usually not required. However, if a service creates and holds onto observables that emit continuously (e.g., WebSocket streams), you should manage their lifecycle manually to prevent memory leaks. Always unsubscribe in components, not services.

Can I use services in Angular libraries or standalone components?

Yes. Services work the same way in standalone components and Angular libraries. When using standalone components, provide services using the providers array in the component decorator or use provideXXX() functions in the application bootstrap.

When should I use a service vs. a state management library like NgRx?

Use services for simple state management—like user authentication, cart items, or configuration. Use NgRx or NGXS when you have complex state with multiple interconnected pieces, need time-travel debugging, or require strict unidirectional data flow across a large application.

Is it okay to use global variables in services?

It’s better to use RxJS subjects (like BehaviorSubject) instead of plain variables to manage state in services. Subjects are observable, reactive, and allow multiple components to react to changes. Global variables can lead to unpredictable behavior and make testing harder.

How do I share a service between lazy-loaded modules?

If a service is provided in root, it’s automatically shared across all modules—including lazy-loaded ones. If you provide it in a feature module, it will only be available within that module’s injector. Always use providedIn: 'root' unless you need module-specific isolation.

Conclusion

Angular services are the backbone of scalable, maintainable, and testable applications. By encapsulating logic outside of components, you create reusable, modular units of functionality that can be easily shared, tested, and extended. From simple data fetching to complex state management and real-time communication, services empower you to build robust applications with clean architecture.

Mastering services means understanding dependency injection, RxJS observables, and separation of concerns. Start with basic CRUD services, then evolve to state management with BehaviorSubjects, error handling with operators, and cross-cutting concerns with interceptors. Always prioritize single responsibility, type safety, and testability.

As your application grows, services will become your most reliable tools for organizing complexity. Don’t treat them as afterthoughts—design them thoughtfully from the beginning. With the practices and examples outlined in this guide, you’re now equipped to build Angular applications that are not only functional but also elegant, efficient, and future-proof.