Understanding Angular Services
Services in Angular are specialized classes that serve a specific purpose in your application. They encapsulate non-UI logic and data that can be shared across components.
Why Use Services?
- Separation of Concerns: Keep components focused on the view and delegate business logic to services
- Reusability: Share functionality across multiple components
- State Management: Maintain application state outside the component tree
- Data Access: Centralize API calls and data manipulation
- Testability: Services can be tested independently of components
Real-world analogy: If components are like the front desk staff at a hotel (interacting with guests), services are like the back-office staff (managing reservations, handling payments, maintaining guest records). The front desk doesn't need to know all the details of how the back-office works, just how to interact with it.
Common Types of Services
- Data Services: Handle API calls and data processing
- State Services: Manage application state
- Utility Services: Provide helper functions (formatting, validation, etc.)
- Feature Services: Encapsulate logic for specific features
- Authentication Services: Handle user authentication and authorization
- Logging Services: Record application events and errors
Creating and Using Services
Creating a Service with Angular CLI
// Generate a service
ng generate service services/data
// or shorthand
ng g s services/data
This creates a service file with the following structure:
// data.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor() { }
}
The @Injectable Decorator
The @Injectable decorator marks a class as available for dependency injection. The providedIn: 'root' option registers the service at the application level, making it a singleton available throughout the app.
Implementing a Service
Let's build a simple product service:
// product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { Product } from '../models/product';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private apiUrl = 'https://api.example.com/products';
constructor(private http: HttpClient) { }
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl)
.pipe(
catchError(this.handleError)
);
}
getProduct(id: number): Observable<Product> {
return this.http.get<Product>(`${this.apiUrl}/${id}`)
.pipe(
catchError(this.handleError)
);
}
createProduct(product: Omit<Product, 'id'>): Observable<Product> {
return this.http.post<Product>(this.apiUrl, product)
.pipe(
catchError(this.handleError)
);
}
updateProduct(product: Product): Observable<Product> {
return this.http.put<Product>(`${this.apiUrl}/${product.id}`, product)
.pipe(
catchError(this.handleError)
);
}
deleteProduct(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`)
.pipe(
catchError(this.handleError)
);
}
private handleError(error: any) {
console.error('An error occurred', error);
return throwError(() => new Error('Something went wrong. Please try again later.'));
}
}
Using a Service in Components
To use a service, inject it in the component's constructor:
// product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html'
})
export class ProductListComponent implements OnInit {
products: Product[] = [];
loading = false;
error: string | null = null;
constructor(private productService: ProductService) { }
ngOnInit(): void {
this.loading = true;
this.productService.getProducts()
.subscribe({
next: (products) => {
this.products = products;
this.loading = false;
},
error: (err) => {
this.error = err.message;
this.loading = false;
}
});
}
deleteProduct(id: number): void {
this.productService.deleteProduct(id)
.subscribe({
next: () => {
this.products = this.products.filter(p => p.id !== id);
},
error: (err) => {
this.error = `Failed to delete product: ${err.message}`;
}
});
}
}
Understanding Dependency Injection (DI)
Dependency Injection is a fundamental concept in Angular. It's a design pattern that lets you declare dependencies without creating them directly.
How DI Works in Angular
Angular's DI system consists of three main parts:
- Provider: Tells Angular how to create or obtain a dependency
- Injector: Maintains a container of dependency instances and creates new ones when needed
- Consumer: The class that declares dependencies through constructor parameters
Benefits of Dependency Injection
- Decoupling: Classes don't need to know how to create their dependencies
- Testability: Dependencies can be easily mocked for testing
- Flexibility: Implementations can be swapped without changing consumers
- Singleton Pattern: Ensures only one instance of a service exists when needed
- Lazy Instantiation: Services are only created when they're required
Real-world analogy: Imagine you're building a car (component). Instead of manufacturing your own engine, wheels, and electronics (services), you specify what you need, and the car factory (injector) provides these components. This allows you to focus on building your car without worrying about how each individual part is made.
Service Providers and Hierarchical Injection
Angular's dependency injection system is hierarchical, following the component tree structure.
Provider Scopes
Services can be provided at different levels:
- Root Level: Application-wide singleton (
providedIn: 'root') - Module Level: Available to all components in the module (providers array in @NgModule)
- Component Level: Available only to the component and its children (providers array in @Component)
Root-Level Provider
@Injectable({
providedIn: 'root' // Available application-wide
})
export class GlobalService { }
Module-Level Provider
@NgModule({
declarations: [/* components */],
imports: [/* other modules */],
providers: [
FeatureService // Available to all components in this module
]
})
export class FeatureModule { }
Component-Level Provider
@Component({
selector: 'app-feature',
templateUrl: './feature.component.html',
providers: [
LocalService // Available only to this component and its children
]
})
export class FeatureComponent { }
Resolution Rules
When a component requests a service, Angular looks for a provider in this order:
- The component's own injector (if the service is provided at the component level)
- Parent component injectors, up the component tree
- The module injector
- The root injector
If no provider is found, Angular throws an error.
Practical Example: Component-Specific Services
// local-counter.service.ts
@Injectable() // No providedIn: 'root'
export class LocalCounterService {
private count = 0;
increment(): void {
this.count++;
}
decrement(): void {
this.count--;
}
getCount(): number {
return this.count;
}
}
// parent.component.ts
@Component({
selector: 'app-parent',
template: `
<h2>Parent Counter: {{ getCount() }}</h2>
<button (click)="increment()">Increment</button>
<app-child></app-child>
<app-child></app-child>
`,
providers: [LocalCounterService] // Provided at component level
})
export class ParentComponent {
constructor(private counter: LocalCounterService) { }
increment(): void {
this.counter.increment();
}
getCount(): number {
return this.counter.getCount();
}
}
// child.component.ts
@Component({
selector: 'app-child',
template: `
<div class="child">
<h3>Child Counter: {{ getCount() }}</h3>
<button (click)="increment()">Increment</button>
</div>
`
})
export class ChildComponent {
constructor(private counter: LocalCounterService) { }
increment(): void {
this.counter.increment();
}
getCount(): number {
return this.counter.getCount();
}
}
In this example, both child components use the same instance of LocalCounterService as the parent, because the service is provided at the parent level. If we move the provider to the ChildComponent, each child would have its own counter instance.
Advanced Dependency Injection
Using Value and Factory Providers
Angular's DI system supports various provider types:
1. Class Provider (Default)
providers: [
SomeService // Shorthand for { provide: SomeService, useClass: SomeService }
]
2. Value Provider
// Provide a fixed value
providers: [
{ provide: 'API_URL', useValue: 'https://api.example.com' }
]
// Inject with @Inject
constructor(@Inject('API_URL') private apiUrl: string) { }
3. Factory Provider
// Define a factory function
export function configFactory(http: HttpClient) {
return () => http.get<AppConfig>('/assets/config.json');
}
// Provide the factory
providers: [
{
provide: APP_INITIALIZER,
useFactory: configFactory,
deps: [HttpClient],
multi: true
}
]
4. Existing Provider (Alias)
// Use an existing service under a different token
providers: [
{ provide: AbstractLogger, useExisting: ConsoleLogger }
]
Configuration Injection Example
// config.ts
export interface AppConfig {
apiUrl: string;
theme: 'light' | 'dark';
features: {
notifications: boolean;
analytics: boolean;
};
}
// Injection token
import { InjectionToken } from '@angular/core';
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// Providing configuration
@NgModule({
providers: [
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
theme: 'light',
features: {
notifications: true,
analytics: true
}
}
}
]
})
export class AppModule { }
// Using the config
@Injectable()
export class ApiService {
constructor(@Inject(APP_CONFIG) private config: AppConfig) {
console.log('API URL:', config.apiUrl);
}
}
Using InjectionToken for Type Safety
InjectionToken provides type safety for non-class dependencies:
// Define a typed token
export const API_URL = new InjectionToken<string>('api.url');
// Provide a value
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
// Inject in a service
constructor(@Inject(API_URL) private apiUrl: string) { }
Optional Dependencies
You can mark dependencies as optional:
import { Optional } from '@angular/core';
@Injectable()
export class FlexibleService {
constructor(@Optional() private logger: LoggerService) {
if (logger) {
logger.log('FlexibleService instantiated');
}
}
}
Self, SkipSelf, and Host
Angular provides decorators to control where dependencies are resolved from:
import { Self, SkipSelf, Host } from '@angular/core';
@Component({
providers: [SomeService] // Component provides its own instance
})
export class MyComponent {
constructor(
@Self() private fromSelf: SomeService, // Only check this component's injector
@SkipSelf() private fromAncestor: SomeService, // Skip this component's injector
@Host() private fromHost: SomeService // Check only up to the host component
) { }
}
Stateful Services for State Management
Services can be used to manage state across components in Angular applications.
Simple State Service
// cart.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Product } from '../models/product';
export interface CartItem {
product: Product;
quantity: number;
}
@Injectable({
providedIn: 'root'
})
export class CartService {
// BehaviorSubject for the internal state
private cartItems = new BehaviorSubject<CartItem[]>([]);
// Expose as Observable for components to subscribe to
cartItems$ = this.cartItems.asObservable();
// Computed Observable for total items
totalItems$: Observable<number> = this.cartItems$.pipe(
map(items => items.reduce((total, item) => total + item.quantity, 0))
);
// Computed Observable for total price
totalPrice$: Observable<number> = this.cartItems$.pipe(
map(items => items.reduce(
(total, item) => total + (item.product.price * item.quantity),
0
))
);
addToCart(product: Product, quantity = 1): void {
const currentItems = this.cartItems.getValue();
const existingItemIndex = currentItems.findIndex(
item => item.product.id === product.id
);
if (existingItemIndex > -1) {
// Update quantity if product already exists
const updatedItems = [...currentItems];
updatedItems[existingItemIndex] = {
...updatedItems[existingItemIndex],
quantity: updatedItems[existingItemIndex].quantity + quantity
};
this.cartItems.next(updatedItems);
} else {
// Add new product to cart
this.cartItems.next([
...currentItems,
{ product, quantity }
]);
}
}
updateQuantity(productId: number, quantity: number): void {
if (quantity <= 0) {
this.removeFromCart(productId);
return;
}
const currentItems = this.cartItems.getValue();
const updatedItems = currentItems.map(item =>
item.product.id === productId
? { ...item, quantity }
: item
);
this.cartItems.next(updatedItems);
}
removeFromCart(productId: number): void {
const currentItems = this.cartItems.getValue();
const updatedItems = currentItems.filter(
item => item.product.id !== productId
);
this.cartItems.next(updatedItems);
}
clearCart(): void {
this.cartItems.next([]);
}
}
Using the State Service in Components
// product-detail.component.ts
@Component({
selector: 'app-product-detail',
template: `
<div *ngIf="product">
<h2>{{ product.name }}</h2>
<p>{{ product.price | currency }}</p>
<button (click)="addToCart()">Add to Cart</button>
</div>
`
})
export class ProductDetailComponent {
@Input() product!: Product;
constructor(private cartService: CartService) { }
addToCart(): void {
this.cartService.addToCart(this.product);
}
}
// cart-summary.component.ts
@Component({
selector: 'app-cart-summary',
template: `
<div class="cart-summary">
<span>{{ totalItems$ | async }} items</span>
<span>{{ totalPrice$ | async | currency }}</span>
</div>
`
})
export class CartSummaryComponent {
totalItems$: Observable<number>;
totalPrice$: Observable<number>;
constructor(private cartService: CartService) {
this.totalItems$ = this.cartService.totalItems$;
this.totalPrice$ = this.cartService.totalPrice$;
}
}
Advantages of Service-Based State Management
- Simple to implement and understand
- Works well for small to medium applications
- Built on Angular's core concepts (services, RxJS)
- No additional libraries required
- Good performance characteristics
When to Use More Advanced State Management
For larger applications, you might consider more robust solutions like NgRx or NGXS when:
- You need centralized state debugging
- There are complex state transitions and side effects
- You require time-travel debugging
- State is very complex with many interdependent parts
- You want strict state immutability guarantees
Integrating with HttpClient
Angular's HttpClient is used for making API requests, and is typically wrapped in services:
Basic HttpClient Example
// Import HttpClientModule in your app/feature module
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
HttpClientModule
// other imports
]
})
export class AppModule { }
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from './user.model';
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) { }
getUsers(page = 1, limit = 10): Observable<User[]> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString());
return this.http.get<User[]>(this.apiUrl, { params });
}
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(user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
Error Handling
import { catchError, retry } from 'rxjs/operators';
import { throwError } from 'rxjs';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl)
.pipe(
retry(2), // Retry failed requests up to 2 times
catchError(this.handleError)
);
}
private handleError(error: any): Observable<never> {
let errorMessage = 'An unknown error occurred';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
HTTP Interceptors
Interceptors allow you to modify HTTP requests and responses globally:
// auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Get the auth token from the service
const authToken = this.authService.getToken();
// Clone the request and replace the original headers with
// cloned headers, updated with the authorization.
if (authToken) {
const authReq = request.clone({
headers: request.headers.set('Authorization', `Bearer ${authToken}`)
});
// Send cloned request with header to the next handler.
return next.handle(authReq);
}
return next.handle(request);
}
}
// Register in app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AppModule { }
Multiple Interceptors
You can register multiple interceptors that run in order:
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
]
})
export class AppModule { }
Progress and Loading Indicators
// loading.service.ts
@Injectable({
providedIn: 'root'
})
export class LoadingService {
private loadingSubject = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSubject.asObservable();
setLoading(isLoading: boolean): void {
this.loadingSubject.next(isLoading);
}
}
// loading.interceptor.ts
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
private totalRequests = 0;
constructor(private loadingService: LoadingService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
this.totalRequests++;
this.loadingService.setLoading(true);
return next.handle(request).pipe(
finalize(() => {
this.totalRequests--;
if (this.totalRequests === 0) {
this.loadingService.setLoading(false);
}
})
);
}
}
Testing Services
Angular services are easier to test than components because they don't involve the DOM:
Unit Testing a Service
// cart.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { CartService } from './cart.service';
import { Product } from '../models/product';
describe('CartService', () => {
let service: CartService;
// Sample product for testing
const testProduct: Product = {
id: 1,
name: 'Test Product',
price: 100,
imageUrl: 'test.jpg',
description: 'Test description'
};
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CartService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add an item to the cart', (done) => {
service.addToCart(testProduct, 2);
service.cartItems$.subscribe(items => {
expect(items.length).toBe(1);
expect(items[0].product).toEqual(testProduct);
expect(items[0].quantity).toBe(2);
done();
});
});
it('should update quantity when adding an existing item', (done) => {
service.addToCart(testProduct, 1);
service.addToCart(testProduct, 2);
service.cartItems$.subscribe(items => {
expect(items.length).toBe(1);
expect(items[0].quantity).toBe(3);
done();
});
});
it('should calculate total price correctly', (done) => {
service.addToCart(testProduct, 2); // 2 * $100 = $200
service.totalPrice$.subscribe(total => {
expect(total).toBe(200);
done();
});
});
it('should remove an item from the cart', (done) => {
service.addToCart(testProduct);
service.removeFromCart(testProduct.id);
service.cartItems$.subscribe(items => {
expect(items.length).toBe(0);
done();
});
});
it('should clear the cart', (done) => {
service.addToCart(testProduct);
service.clearCart();
service.cartItems$.subscribe(items => {
expect(items.length).toBe(0);
done();
});
});
});
Testing Services with Dependencies
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { User } from '../models/user';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Ensure that there are no outstanding requests
});
it('should retrieve users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
service.getUsers().subscribe(users => {
expect(users.length).toBe(2);
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers); // Provide mock data
});
it('should create a user', () => {
const newUser = { name: 'Alice', email: 'alice@example.com' };
const mockResponse = { id: 3, ...newUser };
service.createUser(newUser).subscribe(user => {
expect(user.id).toBe(3);
expect(user.name).toBe(newUser.name);
});
const req = httpMock.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(newUser);
req.flush(mockResponse);
});
});
Testing with Service Mocks
// Testing a component that uses a service
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { ProductListComponent } from './product-list.component';
import { ProductService } from '../services/product.service';
// Create a mock service
class MockProductService {
getProducts() {
return of([
{ id: 1, name: 'Test Product', price: 100 }
]);
}
deleteProduct() {
return of(null);
}
}
describe('ProductListComponent', () => {
let component: ProductListComponent;
let fixture: ComponentFixture<ProductListComponent>;
let productService: ProductService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProductListComponent],
providers: [
// Provide the mock service instead of the real one
{ provide: ProductService, useClass: MockProductService }
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProductListComponent);
component = fixture.componentInstance;
productService = TestBed.inject(ProductService);
fixture.detectChanges();
});
it('should load products on init', () => {
expect(component.products.length).toBe(1);
expect(component.products[0].name).toBe('Test Product');
});
});
Service Design Patterns
Repository Pattern
This pattern abstracts the data access layer:
// user.repository.ts
@Injectable({
providedIn: 'root'
})
export class UserRepository {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
findAll(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
findById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
create(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
update(user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
// user.service.ts (higher-level service)
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(private userRepository: UserRepository) {}
getAllUsers(): Observable<User[]> {
return this.userRepository.findAll().pipe(
map(users => users.sort((a, b) => a.name.localeCompare(b.name)))
);
}
getActiveUsers(): Observable<User[]> {
return this.userRepository.findAll().pipe(
map(users => users.filter(user => user.isActive))
);
}
}
Facade Pattern
This pattern simplifies complex subsystems by providing a simpler interface:
// auth-facade.service.ts
@Injectable({
providedIn: 'root'
})
export class AuthFacade {
// Expose simple observables for components
isLoggedIn$: Observable<boolean>;
currentUser$: Observable<User | null>;
authError$: Observable<string | null>;
constructor(
private authService: AuthService,
private router: Router,
private store: Store<AppState>
) {
this.isLoggedIn$ = this.authService.isAuthenticated$;
this.currentUser$ = this.authService.currentUser$;
this.authError$ = this.authService.authError$;
}
// Simplified methods for components
login(email: string, password: string): void {
this.authService.login(email, password).subscribe({
next: () => this.router.navigate(['/dashboard']),
error: (err) => console.error('Login failed', err)
});
}
logout(): void {
this.authService.logout();
this.router.navigate(['/login']);
}
register(userData: UserRegistration): void {
this.authService.register(userData).subscribe({
next: () => this.router.navigate(['/login']),
error: (err) => console.error('Registration failed', err)
});
}
}
Strategy Pattern
This pattern allows you to select an algorithm at runtime:
// payment-strategy.interface.ts
export interface PaymentStrategy {
processPayment(amount: number): Observable<PaymentResult>;
validatePaymentData(data: any): boolean;
}
// credit-card-payment.service.ts
@Injectable()
export class CreditCardPaymentService implements PaymentStrategy {
constructor(private http: HttpClient) {}
processPayment(amount: number): Observable<PaymentResult> {
// Credit card payment logic
return this.http.post<PaymentResult>('/api/payments/credit-card', { amount });
}
validatePaymentData(data: any): boolean {
// Validate credit card data
return !!data.cardNumber && !!data.expiryDate && !!data.cvv;
}
}
// paypal-payment.service.ts
@Injectable()
export class PayPalPaymentService implements PaymentStrategy {
constructor(private http: HttpClient) {}
processPayment(amount: number): Observable<PaymentResult> {
// PayPal payment logic
return this.http.post<PaymentResult>('/api/payments/paypal', { amount });
}
validatePaymentData(data: any): boolean {
// Validate PayPal data
return !!data.email;
}
}
// payment.service.ts
@Injectable({
providedIn: 'root'
})
export class PaymentService {
private strategy: PaymentStrategy;
constructor(
private creditCardPayment: CreditCardPaymentService,
private paypalPayment: PayPalPaymentService
) {
this.strategy = this.creditCardPayment; // Default strategy
}
setPaymentMethod(method: 'credit-card' | 'paypal'): void {
this.strategy = method === 'credit-card'
? this.creditCardPayment
: this.paypalPayment;
}
processPayment(amount: number, paymentData: any): Observable<PaymentResult> {
if (!this.strategy.validatePaymentData(paymentData)) {
return throwError(() => new Error('Invalid payment data'));
}
return this.strategy.processPayment(amount);
}
}
Practice Activity
Building a Shopping Cart Service
In this activity, you'll create a complete shopping cart service:
- Set up a new Angular project if you haven't already:
ng new shopping-cart-app cd shopping-cart-app - Create product models:
ng g interface models/product ng g interface models/cart-itemDefine the interfaces:
// models/product.ts export interface Product { id: number; name: string; price: number; imageUrl: string; description: string; inStock: boolean; } // models/cart-item.ts import { Product } from './product'; export interface CartItem { product: Product; quantity: number; } - Create a product service that fetches data:
ng g service services/productImplement the service with mock data:
// services/product.service.ts import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from '../models/product'; @Injectable({ providedIn: 'root' }) export class ProductService { private products: Product[] = [ { id: 1, name: 'Laptop', price: 999.99, imageUrl: 'https://via.placeholder.com/200x150', description: 'Powerful laptop for work and play', inStock: true }, { id: 2, name: 'Smartphone', price: 699.99, imageUrl: 'https://via.placeholder.com/200x150', description: 'Latest smartphone with amazing camera', inStock: true }, { id: 3, name: 'Headphones', price: 149.99, imageUrl: 'https://via.placeholder.com/200x150', description: 'Noise cancelling wireless headphones', inStock: true }, { id: 4, name: 'Tablet', price: 349.99, imageUrl: 'https://via.placeholder.com/200x150', description: 'Lightweight tablet for reading and browsing', inStock: false } ]; getProducts(): Observable<Product[]> { return of(this.products); } getProduct(id: number): Observable<Product | undefined> { return of(this.products.find(p => p.id === id)); } } - Create a cart service:
ng g service services/cartImplement the service:
// services/cart.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { Product } from '../models/product'; import { CartItem } from '../models/cart-item'; @Injectable({ providedIn: 'root' }) export class CartService { // TODO: Implement the cart service as shown in the lecture // Include methods for: // - Adding items to cart // - Updating quantities // - Removing items // - Calculating totals // - Clearing the cart } - Create components that use these services:
ng g c components/product-list ng g c components/product-item ng g c components/cart ng g c components/cart-item - Implement the components to display products and manage the cart
Challenge
- Add a persistent cart that saves to localStorage
- Implement a checkout process with form validation
- Add HTTP services for fetching products from a real API
- Create an order history service that tracks past orders
Key Takeaways
- Services in Angular are specialized classes that encapsulate non-UI logic and data
- Dependency Injection is Angular's mechanism for providing services to components
- Services can be provided at root, module, or component level, creating different scopes
- Hierarchical DI enables effective service sharing and isolation
- Services are ideal for data access, state management, and shared functionality
- RxJS Observables integrate well with Angular services for reactive programming
- HTTP services abstract API communication and handle data transformation
- Services are easier to test than components, especially with Angular's testing utilities
- Well-designed services follow established design patterns and best practices