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
 1

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 capabilitiessuch as fetching data from an API, managing application state, or logging eventsthat 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, well walk you through everything you need to knowfrom creating your first service to implementing advanced patterns and best practicesso 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 classessuch 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. Lets 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, weve:

  • 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 doesnt handle UI logic. It simply provides a clean API for data operations. This separation of concerns is key to Angulars architecture.

Injecting the Service into a Component

Now that the service is ready, you need to use it inside a component. Lets 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 components 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.

Lets 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

Angulars 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 its actually used. This enables tree-shakingremoving unused code during the build processwhich 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] //

})

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 statefor 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 theyre 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.

Lets 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 leaksespecially 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 theres 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. Thats 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

Dont 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 theyre independent of the UI. Use Angulars 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 arent 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 services 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 logiclike data fetching, authentication, or utility functionsand 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 commonfor example, a UserService might inject an ApiService to make HTTP requests. Angulars dependency injection system handles the chain automatically.

How do I test a service that uses HttpClient?

Use Angulars HttpClientTestingModule and HttpTestingController to mock HTTP requests. You can simulate responses, verify request URLs and methods, and ensure error handling works correctlyall 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 managementlike 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?

Its 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, its automatically shared across all modulesincluding lazy-loaded ones. If you provide it in a feature module, it will only be available within that modules 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. Dont treat them as afterthoughtsdesign them thoughtfully from the beginning. With the practices and examples outlined in this guide, youre now equipped to build Angular applications that are not only functional but also elegant, efficient, and future-proof.