When I first started building Angular applications, I put all my business logic directly in components. That worked fine for small apps, but as applications grew, I found myself duplicating code, struggling with component communication, and making components harder to test. That's when I discovered Angular Services and Dependency Injection, and it completely changed how I structure applications.
Angular Services are singleton classes that provide reusable business logic, data access, and shared functionality across your application. They use Angular's Dependency Injection system, which means you don't manually create service instancesβAngular does it for you and provides them wherever you need them. This makes your code more modular, testable, and maintainable.
π Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
What are Angular Services?
Angular Services provide:
- Reusable business logic - Share code across components
- Data access layer - Centralize API calls and data operations
- State management - Share state between components
- Singleton pattern - Single instance shared across the app
- Dependency Injection - Automatic service provisioning
- Testability - Easy to mock and test independently
- Separation of concerns - Keep components focused on presentation
Creating a Basic Service
Create a service using the @Injectable decorator:
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpHelper } from 'src/core/factory/http.helper';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class BusinessService {
private url: string;
constructor(private http: HttpHelper) {
this.url = `${environment.ApiUrl}business`;
}
public GetBusinesses(data: any): Observable<any> {
return this.http.post(`${this.url}/get`, data);
}
public GetBusiness(businessId: number): Observable<any> {
return this.http.get(`${this.url}/${businessId}/details`);
}
public SaveBusiness(data: any, businessId: number): Observable<any> {
return this.http.post(`${this.url}/${businessId}/details`, data);
}
public DeleteBusiness(id: number): Observable<any> {
return this.http.delete(`${this.url}/${id}`);
}
}
Key Points:
-
@Injectabledecorator marks the class as a service -
providedIn: 'root'makes it an application-wide singleton - Services can inject other services through constructor injection
- Services are instantiated once and shared across the app
Using Services in Components
Inject services into components via constructor:
import { Component, OnInit } from '@angular/core';
import { BusinessService } from 'src/services/business.service';
import { ToastService } from 'src/core/controls/toast-global/toast.service';
@Component({
selector: 'app-business-list',
templateUrl: './business-list.component.html',
styleUrls: ['./business-list.component.scss']
})
export class BusinessListComponent implements OnInit {
public businesses: any[] = [];
public loading: boolean = false;
constructor(
private businessService: BusinessService,
private toast: ToastService
) {}
ngOnInit(): void {
this.loadBusinesses();
}
private loadBusinesses(): void {
this.loading = true;
this.businessService.GetBusinesses({ pageNumber: 1, pageSize: 10 })
.subscribe({
next: (response) => {
this.businesses = response.data;
this.loading = false;
},
error: (error) => {
this.toast.error(error);
this.loading = false;
}
});
}
}
Dependency Injection Process:
- Declare service in constructor with
privateorpublicaccessor - Angular's injector automatically provides the service instance
- Use the service throughout the component
- No need to manually instantiate services
Service with HTTP Client
Services commonly handle HTTP requests:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class UserService {
private url: string;
constructor(private http: HttpClient) {
this.url = `${environment.ApiUrl}users`;
}
public SearchUsers(searchTerm: string, isAll: boolean = false): Observable<any[]> {
if (!searchTerm || searchTerm.length < 3) {
return of([]);
}
return this.http.post(`${this.url}/search?isAll=${isAll}`, { search: searchTerm })
.pipe(
map((response: any) => response.data || []),
tap(users => console.log('Found users:', users)),
catchError(error => {
console.error('Search error:', error);
return of([]);
})
);
}
public GetUser(userId: number): Observable<any> {
return this.http.get(`${this.url}/${userId}`);
}
public CreateUser(userData: any): Observable<any> {
return this.http.post(`${this.url}`, userData);
}
public UpdateUser(userId: number, userData: any): Observable<any> {
return this.http.put(`${this.url}/${userId}`, userData);
}
public DeleteUser(userId: number): Observable<any> {
return this.http.delete(`${this.url}/${userId}`);
}
}
Best Practices:
- Use RxJS operators for data transformation
- Handle errors with
catchError - Return Observables for async operations
- Keep service methods focused and single-purpose
Service Providers and providedIn
Different ways to provide services:
// Application-wide singleton (recommended)
@Injectable({
providedIn: 'root'
})
export class GlobalService { }
// Platform-wide singleton (shared across multiple apps)
@Injectable({
providedIn: 'platform'
})
export class PlatformService { }
// Module-scoped service
@Injectable()
export class ModuleService { }
// Then provide in module
@NgModule({
providers: [ModuleService]
})
export class FeatureModule { }
// Component-scoped service (new instance per component)
@Component({
selector: 'app-component',
providers: [ComponentService]
})
export class MyComponent { }
providedIn Options
| Option | Scope | Use Case |
|---|---|---|
'root' |
Application-wide | Most services (recommended) |
'platform' |
Platform-wide | Shared across multiple apps |
'any' |
Module-scoped | Lazy-loaded modules |
| Module reference | Module-scoped | Feature modules |
| Component providers | Component-scoped | Component-specific instances |
Why providedIn: 'root' is Preferred:
- Enables tree-shaking (removes unused services)
- Creates singleton automatically
- No need to add to module providers
- Better performance and bundle size
Service Communication Patterns
Services can communicate using RxJS:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notificationsSubject = new BehaviorSubject<any[]>([]);
public notifications$ = this.notificationsSubject.asObservable();
addNotification(notification: any): void {
const current = this.notificationsSubject.value;
this.notificationsSubject.next([...current, notification]);
}
removeNotification(id: string): void {
const current = this.notificationsSubject.value;
this.notificationsSubject.next(current.filter(n => n.id !== id));
}
clearNotifications(): void {
this.notificationsSubject.next([]);
}
}
Using BehaviorSubject for State Management
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class UserStateService {
private userSubject = new BehaviorSubject<any>(null);
public user$ = this.userSubject.asObservable();
public isAuthenticated$ = this.user$.pipe(
map(user => !!user)
);
setUser(user: any): void {
this.userSubject.next(user);
}
getUser(): any {
return this.userSubject.value;
}
clearUser(): void {
this.userSubject.next(null);
}
}
Component Subscription
export class UserComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
user: any;
isAuthenticated: boolean = false;
constructor(private userStateService: UserStateService) {}
ngOnInit(): void {
this.userStateService.user$
.pipe(takeUntil(this.destroy$))
.subscribe(user => {
this.user = user;
});
this.userStateService.isAuthenticated$
.pipe(takeUntil(this.destroy$))
.subscribe(isAuth => {
this.isAuthenticated = isAuth;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Injecting Services into Other Services
Services can inject other services:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { LoggerService } from './logger.service';
import { CacheService } from './cache.service';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(
private http: HttpClient,
private logger: LoggerService,
private cache: CacheService
) {}
getData(id: number): Observable<any> {
const cached = this.cache.get(id);
if (cached) {
return of(cached);
}
return this.http.get(`/api/data/${id}`).pipe(
tap(data => {
this.cache.set(id, data);
this.logger.log('Data fetched:', data);
})
);
}
}
Service Factory Pattern
Create services with factory functions:
import { Injectable, InjectionToken, FactoryProvider } from '@angular/core';
export const API_CONFIG = new InjectionToken<string>('api.config');
export function apiConfigFactory(): string {
return environment.production
? 'https://api.production.com'
: 'https://api.dev.com';
}
export const API_CONFIG_PROVIDER: FactoryProvider = {
provide: API_CONFIG,
useFactory: apiConfigFactory
};
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(@Inject(API_CONFIG) private apiUrl: string) {
console.log('API URL:', this.apiUrl);
}
}
Service Testing
Test services independently:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
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();
});
it('should fetch users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
service.GetUser(1).subscribe(user => {
expect(user).toEqual(mockUsers[0]);
});
const req = httpMock.expectOne('/api/users/1');
expect(req.request.method).toBe('GET');
req.flush(mockUsers[0]);
});
});
Common Service Patterns
1. Data Service Pattern
@Injectable({
providedIn: 'root'
})
export class DataService {
private cache = new Map<string, any>();
constructor(private http: HttpClient) {}
get<T>(url: string, useCache: boolean = true): Observable<T> {
if (useCache && this.cache.has(url)) {
return of(this.cache.get(url));
}
return this.http.get<T>(url).pipe(
tap(data => {
if (useCache) {
this.cache.set(url, data);
}
})
);
}
clearCache(): void {
this.cache.clear();
}
}
2. Utility Service Pattern
@Injectable({
providedIn: 'root'
})
export class UtilityService {
formatDate(date: Date): string {
return date.toLocaleDateString();
}
formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}
generateId(): string {
return Math.random().toString(36).substr(2, 9);
}
}
3. Authentication Service Pattern
@Injectable({
providedIn: 'root'
})
export class AuthService {
private tokenSubject = new BehaviorSubject<string | null>(null);
public token$ = this.tokenSubject.asObservable();
constructor(
private http: HttpClient,
private router: Router
) {
const token = localStorage.getItem('token');
if (token) {
this.tokenSubject.next(token);
}
}
login(credentials: any): Observable<any> {
return this.http.post('/api/auth/login', credentials).pipe(
tap(response => {
const token = response.token;
localStorage.setItem('token', token);
this.tokenSubject.next(token);
})
);
}
logout(): void {
localStorage.removeItem('token');
this.tokenSubject.next(null);
this.router.navigate(['/login']);
}
isAuthenticated(): boolean {
return !!this.tokenSubject.value;
}
}
Best Practices
-
Use
providedIn: 'root'- For application-wide singleton services - Keep services focused - Single responsibility principle
- Use services for business logic - Not presentation logic
- Handle errors appropriately - In service methods
- Return Observables - For async operations
- Use RxJS operators - For data transformation
- Keep services stateless - When possible
- Use BehaviorSubject - For shared state management
- Document service methods - And their return types
- Test services independently - From components
- Inject dependencies - Through constructor
- Use TypeScript types - For better type safety
- Avoid circular dependencies - Between services
- Use lazy loading - For feature-specific services when appropriate
Common Patterns
Service Organization
src/
services/
core/
auth.service.ts
http.service.ts
logger.service.ts
features/
business.service.ts
user.service.ts
site.service.ts
shared/
notification.service.ts
utility.service.ts
Service with Error Handling
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(
private http: HttpClient,
private errorHandler: ErrorHandlerService
) {}
get<T>(url: string): Observable<T> {
return this.http.get<T>(url).pipe(
catchError(error => {
this.errorHandler.handle(error);
return throwError(() => error);
})
);
}
}
Resources and Further Reading
- π Full Angular Services Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- Angular HTTP Client Guide - HTTP requests with services
- Angular Reactive Forms Guide - Forms with service integration
- Angular Component Communication - Services for component communication
- Angular Dependency Injection Documentation - Official Angular docs
- Angular Services Guide - Official services tutorial
- RxJS Documentation - Reactive programming with services
Conclusion
Angular Services and Dependency Injection provide a powerful way to organize code, share business logic, and manage application state. By following these patterns, you can build maintainable, testable, and scalable Angular applications. Services are the foundation of clean architecture in Angular applications.
Key Takeaways:
- Angular Services - Singleton classes for reusable business logic
- Dependency Injection - Automatic service provisioning
- providedIn: 'root' - Application-wide singleton (recommended)
- Service Communication - Using RxJS Observables and Subjects
- Service Patterns - Data services, utility services, authentication services
- Testing - Services can be tested independently
- Best Practices - Keep services focused, handle errors, use Observables
Whether you're building a simple dashboard or a complex enterprise application, Angular Services provide the foundation you need. They handle all the business logic while keeping your components clean and focused on presentation.
What's your experience with Angular Services and Dependency Injection? Share your tips and tricks in the comments below! π
π‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.
Top comments (0)