Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Angular Services with Dependency Injection! ๐ In this guide, weโll explore how TypeScript makes Angularโs dependency injection system incredibly powerful and type-safe.
Youโll discover how services can transform your Angular development experience. Whether youโre building web applications ๐, managing data ๐, or creating reusable utilities ๐ ๏ธ, understanding services and dependency injection is essential for writing robust, maintainable Angular code.
By the end of this tutorial, youโll feel confident creating and injecting services in your own Angular projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Angular Services
๐ค What are Angular Services?
Angular services are like specialized helpers in your application ๐จ. Think of them as the hardworking staff behind the scenes at a restaurant ๐ฝ๏ธ - while the waiters (components) interact with customers, the kitchen staff (services) prepare the food, manage inventory, and handle the complex operations.
In TypeScript terms, services are classes decorated with @Injectable()
that provide specific functionality throughout your application. This means you can:
- โจ Share data between components
- ๐ Handle HTTP requests and API calls
- ๐ก๏ธ Manage application state
- ๐ง Provide reusable business logic
๐ก Why Use Dependency Injection?
Hereโs why developers love Angularโs dependency injection system:
- Type Safety ๐: TypeScript ensures you get the right service types
- Better Testing ๐งช: Easy to mock services for unit tests
- Code Reusability ๐: Share logic across multiple components
- Loose Coupling ๐ค: Components donโt need to know how services work
Real-world example: Imagine building a shopping app ๐. With services, your cart component, product list, and checkout can all share the same cart service without duplicating code!
๐ง Basic Syntax and Usage
๐ Creating Your First Service
Letโs start with a friendly example:
// ๐ Hello, Angular Service!
import { Injectable } from '@angular/core';
// ๐ฏ The @Injectable decorator makes this a service
@Injectable({
providedIn: 'root' // ๐ This makes it available app-wide!
})
export class GreetingService {
private message: string = "Welcome to TypeScript with Angular! ๐";
// ๐ก Simple method to get greeting
getGreeting(): string {
return this.message;
}
// ๐จ Method to customize greeting
setGreeting(newMessage: string): void {
this.message = newMessage;
}
}
๐ก Explanation: The @Injectable()
decorator tells Angular this class can be injected into other classes. The providedIn: 'root'
makes it a singleton available throughout your app!
๐ฏ Injecting Services into Components
Hereโs how to use your service in a component:
// ๐๏ธ Component that uses our service
import { Component, OnInit } from '@angular/core';
import { GreetingService } from './greeting.service';
@Component({
selector: 'app-welcome',
template: `
<div class="welcome-container">
<h1>{{ greeting }}</h1>
<button (click)="changeGreeting()">Change Greeting ๐ญ</button>
</div>
`
})
export class WelcomeComponent implements OnInit {
greeting: string = '';
// ๐ฏ Inject the service through constructor
constructor(private greetingService: GreetingService) {
// TypeScript knows greetingService is GreetingService type! โจ
}
ngOnInit(): void {
// ๐ Use the service
this.greeting = this.greetingService.getGreeting();
}
// ๐จ Change greeting when button clicked
changeGreeting(): void {
this.greetingService.setGreeting("Angular Services are awesome! ๐");
this.greeting = this.greetingService.getGreeting();
}
}
๐ก Practical Examples
๐ Example 1: Shopping Cart Service
Letโs build something real - a shopping cart service:
// ๐๏ธ Define our product interface
interface Product {
id: string;
name: string;
price: number;
emoji: string; // Every product needs an emoji!
category: 'electronics' | 'clothing' | 'books';
}
// ๐ Shopping cart service
@Injectable({
providedIn: 'root'
})
export class ShoppingCartService {
private items: Product[] = [];
private cartSubject = new BehaviorSubject<Product[]>([]);
// ๐ฏ Observable for components to subscribe to cart changes
cart$ = this.cartSubject.asObservable();
// โ Add item to cart
addItem(product: Product): void {
this.items.push(product);
this.cartSubject.next([...this.items]); // ๐ Notify subscribers
console.log(`Added ${product.emoji} ${product.name} to cart!`);
}
// โ Remove item from cart
removeItem(productId: string): void {
this.items = this.items.filter(item => item.id !== productId);
this.cartSubject.next([...this.items]);
}
// ๐ฐ Calculate total with proper typing
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
// ๐ Get cart statistics
getCartStats(): { itemCount: number; total: number; categories: string[] } {
return {
itemCount: this.items.length,
total: this.getTotal(),
categories: [...new Set(this.items.map(item => item.category))]
};
}
// ๐งน Clear entire cart
clearCart(): void {
this.items = [];
this.cartSubject.next([]);
console.log("๐ Cart cleared!");
}
}
// ๐ฎ Using the service in a component
@Component({
selector: 'app-product-list',
template: `
<div class="product-grid">
<div class="cart-summary">
๐ Items: {{ cartStats.itemCount }} | Total: ${{ cartStats.total }}
</div>
<button (click)="addSampleProduct()">Add Sample Product ๐</button>
</div>
`
})
export class ProductListComponent implements OnInit {
cartStats = { itemCount: 0, total: 0, categories: [] as string[] };
constructor(private cartService: ShoppingCartService) {}
ngOnInit(): void {
// ๐ Subscribe to cart changes
this.cartService.cart$.subscribe(items => {
this.cartStats = this.cartService.getCartStats();
});
}
// ๐ Add a sample product for demo
addSampleProduct(): void {
const sampleProduct: Product = {
id: Date.now().toString(),
name: "TypeScript Handbook",
price: 29.99,
emoji: "๐",
category: 'books'
};
this.cartService.addItem(sampleProduct);
}
}
๐ฏ Try it yourself: Add quantity tracking and discount calculation features!
๐ฎ Example 2: User Authentication Service
Letโs make authentication type-safe and fun:
// ๐ค User interface for type safety
interface User {
id: string;
username: string;
email: string;
role: 'admin' | 'user' | 'moderator';
avatar: string; // Emoji avatar!
isActive: boolean;
}
// ๐ Authentication response
interface AuthResponse {
success: boolean;
user?: User;
token?: string;
message: string;
}
// ๐ก๏ธ Authentication service
@Injectable({
providedIn: 'root'
})
export class AuthService {
private currentUserSubject = new BehaviorSubject<User | null>(null);
private isLoggedInSubject = new BehaviorSubject<boolean>(false);
// ๐ฏ Observables for components
currentUser$ = this.currentUserSubject.asObservable();
isLoggedIn$ = this.isLoggedInSubject.asObservable();
constructor(private http: HttpClient) {
// ๐ Check for existing session on service init
this.checkExistingSession();
}
// ๐ช Login method with type safety
async login(username: string, password: string): Promise<AuthResponse> {
try {
// ๐ฏ Mock API call (replace with real HTTP request)
const response: AuthResponse = await this.mockLogin(username, password);
if (response.success && response.user) {
this.currentUserSubject.next(response.user);
this.isLoggedInSubject.next(true);
// ๐พ Store token securely
if (response.token) {
localStorage.setItem('auth_token', response.token);
}
console.log(`๐ Welcome back, ${response.user.username}! ${response.user.avatar}`);
}
return response;
} catch (error) {
console.error('๐จ Login failed:', error);
return {
success: false,
message: 'Login failed. Please try again! ๐'
};
}
}
// ๐ช Logout method
logout(): void {
this.currentUserSubject.next(null);
this.isLoggedInSubject.next(false);
localStorage.removeItem('auth_token');
console.log('๐ See you later! Logged out successfully');
}
// ๐ Check if user has specific role
hasRole(role: User['role']): boolean {
const currentUser = this.currentUserSubject.value;
return currentUser?.role === role || false;
}
// ๐ฏ Get current user (type-safe)
getCurrentUser(): User | null {
return this.currentUserSubject.value;
}
// ๐ค Mock login for demo (replace with real API)
private async mockLogin(username: string, password: string): Promise<AuthResponse> {
// ๐ญ Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
// ๐ฏ Mock successful login
if (username === 'demo' && password === 'password') {
return {
success: true,
user: {
id: '1',
username: 'demo',
email: '[email protected]',
role: 'user',
avatar: '๐งโ๐ป',
isActive: true
},
token: 'mock-jwt-token-123',
message: 'Login successful! ๐'
};
}
return {
success: false,
message: 'Invalid credentials! ๐'
};
}
// ๐ Check for existing session
private checkExistingSession(): void {
const token = localStorage.getItem('auth_token');
if (token) {
// ๐ฏ Validate token and restore user session
// This would typically involve an API call
console.log('๐ Restoring session...');
}
}
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Service Hierarchical Injection
When youโre ready to level up, try hierarchical injection:
// ๐ฏ Feature-specific service with hierarchy
@Injectable({
providedIn: 'root' // ๐ Root level - singleton across app
})
export class GlobalConfigService {
private config = {
apiUrl: 'https://api.example.com',
theme: 'light' as 'light' | 'dark',
language: 'en'
};
getConfig() {
return { ...this.config }; // ๐ Return copy to prevent mutation
}
}
// ๐๏ธ Component-level service
@Injectable() // ๐ฏ No providedIn - will be provided at component level
export class ComponentSpecificService {
constructor(private globalConfig: GlobalConfigService) {
console.log('๐จ Component service created with config:',
this.globalConfig.getConfig());
}
}
// ๐ฆ Component providing its own service instance
@Component({
selector: 'app-feature',
providers: [ComponentSpecificService], // ๐ฏ Each instance gets its own service
template: '<div>Feature component with its own service! ๐</div>'
})
export class FeatureComponent {
constructor(private componentService: ComponentSpecificService) {}
}
๐๏ธ Advanced Topic 2: Service Factories and Dynamic Injection
For the brave developers:
// ๐ญ Service factory for dynamic configuration
export function createDynamicService(config: any): DynamicService {
return new DynamicService(config);
}
// ๐ฏ Factory provider configuration
@NgModule({
providers: [
{
provide: DynamicService,
useFactory: createDynamicService,
deps: [ConfigService] // ๐ Dependencies for the factory
}
]
})
export class FeatureModule {}
// ๐ช Multi-provider for plugin system
interface PluginService {
name: string;
execute(): void;
}
@Injectable()
export class PluginManager {
constructor(
@Inject('PLUGINS') private plugins: PluginService[] // ๐ Inject all plugins
) {
console.log(`๐ฎ Loaded ${plugins.length} plugins:`,
plugins.map(p => p.name));
}
runAllPlugins(): void {
this.plugins.forEach(plugin => {
console.log(`๐ Running plugin: ${plugin.name}`);
plugin.execute();
});
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Circular Dependencies
// โ Wrong way - circular dependency nightmare!
@Injectable()
export class ServiceA {
constructor(private serviceB: ServiceB) {} // ๐ฅ ServiceB depends on ServiceA!
}
@Injectable()
export class ServiceB {
constructor(private serviceA: ServiceA) {} // ๐ฅ Creates circular dependency!
}
// โ
Correct way - use a shared service or events!
@Injectable()
export class SharedDataService {
private dataSubject = new BehaviorSubject<any>(null);
data$ = this.dataSubject.asObservable();
updateData(data: any): void {
this.dataSubject.next(data);
}
}
@Injectable()
export class ServiceA {
constructor(private sharedData: SharedDataService) {}
doSomething(): void {
this.sharedData.updateData({ from: 'ServiceA', message: '๐' });
}
}
@Injectable()
export class ServiceB {
constructor(private sharedData: SharedDataService) {
// ๐ Listen to shared data changes
this.sharedData.data$.subscribe(data => {
if (data?.from === 'ServiceA') {
console.log('๐ฏ ServiceB received:', data.message);
}
});
}
}
๐คฏ Pitfall 2: Memory Leaks with Subscriptions
// โ Dangerous - subscriptions not cleaned up!
@Component({
selector: 'app-leaky'
})
export class LeakyComponent implements OnInit {
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.dataService.data$.subscribe(data => {
console.log(data); // ๐ฅ This subscription never gets cleaned up!
});
}
}
// โ
Safe - proper subscription management!
@Component({
selector: 'app-safe'
})
export class SafeComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.dataService.data$
.pipe(takeUntil(this.destroy$)) // ๐ก๏ธ Auto-unsubscribe on destroy
.subscribe(data => {
console.log('โ
Safe subscription:', data);
});
}
ngOnDestroy(): void {
this.destroy$.next(); // ๐งน Clean up all subscriptions
this.destroy$.complete();
}
}
๐ ๏ธ Best Practices
- ๐ฏ Use TypeScript Interfaces: Define clear contracts for your services
- ๐ Provide Services at Right Level: Root for singletons, component for instances
- ๐ก๏ธ Handle Errors Gracefully: Always wrap service calls in try-catch
- ๐จ Use Meaningful Names:
UserAuthService
notAuthSvc
- โจ Keep Services Focused: One responsibility per service
- ๐ Use Observables: For reactive data that changes over time
- ๐งน Clean Up Subscriptions: Prevent memory leaks
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Task Management Service System
Create a type-safe task management system with multiple services:
๐ Requirements:
- โ Task service for CRUD operations
- ๐ท๏ธ Category service for task organization
- ๐ค User service for task assignment
- ๐ Analytics service for productivity tracking
- ๐ Notification service for reminders
- ๐จ Each task needs an emoji and priority level!
๐ Bonus Points:
- Add task filtering and sorting
- Implement task sharing between users
- Create a dashboard with statistics
- Add real-time updates using WebSockets
๐ก Solution
๐ Click to see solution
// ๐ฏ Our comprehensive task management system!
// ๐ Task interface
interface Task {
id: string;
title: string;
description: string;
completed: boolean;
priority: 'low' | 'medium' | 'high' | 'urgent';
categoryId: string;
assigneeId?: string;
dueDate?: Date;
emoji: string;
createdAt: Date;
updatedAt: Date;
}
// ๐ท๏ธ Category interface
interface Category {
id: string;
name: string;
color: string;
emoji: string;
}
// ๐ Analytics interface
interface TaskAnalytics {
totalTasks: number;
completedTasks: number;
completionRate: number;
tasksByPriority: Record<Task['priority'], number>;
tasksByCategory: Record<string, number>;
}
// ๐ Task Service
@Injectable({
providedIn: 'root'
})
export class TaskService {
private tasks: Task[] = [];
private tasksSubject = new BehaviorSubject<Task[]>([]);
tasks$ = this.tasksSubject.asObservable();
// โ Create task
createTask(taskData: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Task {
const newTask: Task = {
...taskData,
id: this.generateId(),
createdAt: new Date(),
updatedAt: new Date()
};
this.tasks.push(newTask);
this.tasksSubject.next([...this.tasks]);
console.log(`โ
Created task: ${newTask.emoji} ${newTask.title}`);
return newTask;
}
// ๐ Update task
updateTask(id: string, updates: Partial<Task>): Task | null {
const taskIndex = this.tasks.findIndex(t => t.id === id);
if (taskIndex === -1) return null;
this.tasks[taskIndex] = {
...this.tasks[taskIndex],
...updates,
updatedAt: new Date()
};
this.tasksSubject.next([...this.tasks]);
return this.tasks[taskIndex];
}
// ๐๏ธ Delete task
deleteTask(id: string): boolean {
const initialLength = this.tasks.length;
this.tasks = this.tasks.filter(t => t.id !== id);
if (this.tasks.length < initialLength) {
this.tasksSubject.next([...this.tasks]);
console.log('๐๏ธ Task deleted successfully');
return true;
}
return false;
}
// ๐ฏ Get tasks by category
getTasksByCategory(categoryId: string): Task[] {
return this.tasks.filter(t => t.categoryId === categoryId);
}
// ๐ Search tasks
searchTasks(query: string): Task[] {
const lowercaseQuery = query.toLowerCase();
return this.tasks.filter(t =>
t.title.toLowerCase().includes(lowercaseQuery) ||
t.description.toLowerCase().includes(lowercaseQuery)
);
}
private generateId(): string {
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
}
}
// ๐ท๏ธ Category Service
@Injectable({
providedIn: 'root'
})
export class CategoryService {
private categories: Category[] = [
{ id: '1', name: 'Work', color: '#3b82f6', emoji: '๐ผ' },
{ id: '2', name: 'Personal', color: '#10b981', emoji: '๐ ' },
{ id: '3', name: 'Learning', color: '#f59e0b', emoji: '๐' }
];
private categoriesSubject = new BehaviorSubject<Category[]>(this.categories);
categories$ = this.categoriesSubject.asObservable();
createCategory(categoryData: Omit<Category, 'id'>): Category {
const newCategory: Category = {
...categoryData,
id: Date.now().toString()
};
this.categories.push(newCategory);
this.categoriesSubject.next([...this.categories]);
return newCategory;
}
getCategoryById(id: string): Category | undefined {
return this.categories.find(c => c.id === id);
}
}
// ๐ Analytics Service
@Injectable({
providedIn: 'root'
})
export class AnalyticsService {
constructor(private taskService: TaskService) {}
getTaskAnalytics(): Observable<TaskAnalytics> {
return this.taskService.tasks$.pipe(
map(tasks => this.calculateAnalytics(tasks))
);
}
private calculateAnalytics(tasks: Task[]): TaskAnalytics {
const completedTasks = tasks.filter(t => t.completed).length;
return {
totalTasks: tasks.length,
completedTasks,
completionRate: tasks.length > 0 ? (completedTasks / tasks.length) * 100 : 0,
tasksByPriority: this.groupByPriority(tasks),
tasksByCategory: this.groupByCategory(tasks)
};
}
private groupByPriority(tasks: Task[]): Record<Task['priority'], number> {
return tasks.reduce((acc, task) => {
acc[task.priority] = (acc[task.priority] || 0) + 1;
return acc;
}, {} as Record<Task['priority'], number>);
}
private groupByCategory(tasks: Task[]): Record<string, number> {
return tasks.reduce((acc, task) => {
acc[task.categoryId] = (acc[task.categoryId] || 0) + 1;
return acc;
}, {} as Record<string, number>);
}
}
// ๐ฎ Demo Component
@Component({
selector: 'app-task-manager',
template: `
<div class="task-manager">
<h1>๐ฏ Task Manager Dashboard</h1>
<div class="analytics" *ngIf="analytics">
<h3>๐ Analytics</h3>
<p>Total: {{ analytics.totalTasks }} | Completed: {{ analytics.completedTasks }}</p>
<p>Completion Rate: {{ analytics.completionRate | number:'1.1-1' }}% ๐</p>
</div>
<button (click)="createSampleTask()">Add Sample Task ๐</button>
<div class="tasks">
<div *ngFor="let task of tasks" class="task-item">
{{ task.emoji }} {{ task.title }}
<span [class.completed]="task.completed">
{{ task.completed ? 'โ
' : 'โณ' }}
</span>
</div>
</div>
</div>
`
})
export class TaskManagerComponent implements OnInit {
tasks: Task[] = [];
analytics: TaskAnalytics | null = null;
constructor(
private taskService: TaskService,
private categoryService: CategoryService,
private analyticsService: AnalyticsService
) {}
ngOnInit(): void {
// ๐ Subscribe to tasks
this.taskService.tasks$.subscribe(tasks => {
this.tasks = tasks;
});
// ๐ Subscribe to analytics
this.analyticsService.getTaskAnalytics().subscribe(analytics => {
this.analytics = analytics;
});
}
createSampleTask(): void {
const sampleTasks = [
{ title: 'Learn Angular Services', emoji: '๐', priority: 'high' as const },
{ title: 'Build Todo App', emoji: '๐ ๏ธ', priority: 'medium' as const },
{ title: 'Practice TypeScript', emoji: '๐ป', priority: 'low' as const }
];
const randomTask = sampleTasks[Math.floor(Math.random() * sampleTasks.length)];
this.taskService.createTask({
...randomTask,
description: 'Sample task for demonstration',
completed: false,
categoryId: '1'
});
}
}
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create Angular services with proper TypeScript typing ๐ช
- โ Use dependency injection effectively and safely ๐ก๏ธ
- โ Apply best practices for service architecture ๐ฏ
- โ Debug service issues like a pro ๐
- โ Build scalable Angular applications with services! ๐
Remember: Services are your friends in Angular! They help you organize code, share data, and build maintainable applications. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Angular Services and Dependency Injection!
Hereโs what to do next:
- ๐ป Practice with the task management exercise above
- ๐๏ธ Build a real Angular app using multiple services
- ๐ Move on to our next tutorial: Angular HTTP Client with TypeScript
- ๐ Share your service-powered Angular projects with others!
Remember: Every Angular expert was once a beginner. Keep coding, keep learning, and most importantly, have fun building awesome apps with services! ๐
Happy coding! ๐๐โจ