Angular services are one of the most important features in Angular applications. They allow developers to organize and share data, logic, and functionality across different components. A service is typically a class that encapsulates reusable logic that can be injected into other parts of the application.
This post will explain how to create a simple service in Angular, understand the @Injectable
decorator, use dependency injection, and implement real-world examples to make your application more modular and maintainable.
What is a Service in Angular?
In Angular, a service is a class that provides a specific functionality. Services can be used for operations like:
- Fetching data from an API
- Managing application state
- Logging messages
- Handling authentication
- Performing calculations or shared operations
By moving business logic out of components and into services, your application becomes easier to maintain, test, and reuse.
Why Use Services?
There are several reasons why services are crucial in Angular applications:
- Reusability: A service can be injected into multiple components, allowing shared logic to exist in a single place.
- Separation of Concerns: Components should focus on presentation, while services handle data and logic.
- Maintainability: Updating logic in one service automatically reflects in all components using it.
- Testability: Since services are isolated, they can be easily tested without UI components.
- Singleton Behavior: By default, services are singletons when provided at the root level. This means one instance is shared across the entire app.
Setting Up the Environment
Before creating a service, make sure you have an Angular application set up.
If you don’t have one yet, create a new project:
ng new my-angular-app
cd my-angular-app
Step 1: Creating a Simple Service
Angular provides a built-in command to generate a service easily.
ng generate service data
or the shorter form:
ng g s data
This command creates two files inside the src/app
folder:
data.service.ts
data.service.spec.ts
The .ts
file contains the logic of your service, and the .spec.ts
file is used for testing purposes.
Step 2: Writing a Simple Service
Here is a basic service example:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
getData() {
return ['Angular', 'React', 'Vue'];
}
}
Step 3: Understanding the Code
Let’s break this code down step by step.
- Importing Injectable
import { Injectable } from '@angular/core';
The Injectable
decorator comes from the Angular core library. It marks the class as available for dependency injection.
- The @Injectable Decorator
@Injectable({
providedIn: 'root'
})
The @Injectable
decorator tells Angular that this class can be injected as a dependency into other classes (like components or other services).
providedIn: 'root'
means that this service is available throughout the entire application.- Angular will create a single instance (singleton) of the service when the app starts.
- Defining the Class
export class DataService {
getData() {
return ['Angular', 'React', 'Vue'];
}
}
Here, the DataService
class defines a simple method getData()
that returns a static list of frameworks. This is just for demonstration, but in real scenarios, this method could fetch data from an API or perform other operations.
Step 4: Injecting the Service into a Component
Once the service is created, you can use it inside a component.
Let’s say we have a component called FrameworkListComponent
.
Component File
import { Component, OnInit } from '@angular/core';
import { DataService } from '../data.service';
@Component({
selector: 'app-framework-list',
templateUrl: './framework-list.component.html'
})
export class FrameworkListComponent implements OnInit {
frameworks: string[] = [];
constructor(private dataService: DataService) {}
ngOnInit() {
this.frameworks = this.dataService.getData();
}
}
Template File
<h2>Popular Frontend Frameworks</h2>
<ul>
<li *ngFor="let framework of frameworks">{{ framework }}</li>
</ul>
Step 5: Understanding Dependency Injection (DI)
Dependency Injection is a design pattern used by Angular to supply dependencies (like services) to components and other services.
When you add a service in a component’s constructor:
constructor(private dataService: DataService) {}
Angular automatically provides an instance of that service.
This happens because of Angular’s built-in injector system.
Key facts about DI in Angular:
- Services are defined using
@Injectable()
. - The
providedIn
property tells Angular where to register the service. - Angular’s injector creates and maintains service instances.
- Components don’t need to create services manually using
new
.
Step 6: Service Scope — providedIn Options
You can control where your service is available by changing the providedIn
property.
1. Root Scope
@Injectable({
providedIn: 'root'
})
This makes the service available application-wide. One instance is shared everywhere.
2. Feature Module Scope
@Injectable({
providedIn: 'any'
})
This provides a new instance of the service in each lazy-loaded module.
3. Custom Module Scope
If you provide a service inside a specific module, only components within that module will have access.
@NgModule({
providers: [DataService]
})
export class SharedModule {}
Step 7: Using Services with Observable Data
While our example used static data, in real-world applications, services often fetch data asynchronously using observables from HttpClient
.
Example:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/frameworks';
constructor(private http: HttpClient) {}
getData(): Observable<string[]> {
return this.http.get<string[]>(this.apiUrl);
}
}
Then, in your component:
this.dataService.getData().subscribe(data => {
this.frameworks = data;
});
This pattern allows your application to react to asynchronous data streams efficiently.
Step 8: Sharing Data Between Components
Services are perfect for sharing data between unrelated components.
Example: Shared Message Service
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MessageService {
private messageSource = new BehaviorSubject<string>('Initial Message');
currentMessage = this.messageSource.asObservable();
changeMessage(message: string) {
this.messageSource.next(message);
}
}
Now, you can inject this service into two components and share messages between them.
Sender Component:
constructor(private messageService: MessageService) {}
updateMessage() {
this.messageService.changeMessage('Hello from Sender!');
}
Receiver Component:
constructor(private messageService: MessageService) {}
ngOnInit() {
this.messageService.currentMessage.subscribe(message => {
this.receivedMessage = message;
});
}
This technique is extremely useful for communication between sibling components.
Step 9: Creating a Reusable Utility Service
You can also use services for common utility functions, such as logging or formatting.
Logger Service Example:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggerService {
logInfo(message: string) {
console.log('Info:', message);
}
logWarning(message: string) {
console.warn('Warning:', message);
}
logError(message: string) {
console.error('Error:', message);
}
}
Then use it anywhere:
constructor(private logger: LoggerService) {}
ngOnInit() {
this.logger.logInfo('Component initialized');
}
Step 10: Testing Angular Services
Angular automatically creates a .spec.ts
file when generating a service. You can use it for unit testing.
Example Test:
import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DataService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return data', () => {
expect(service.getData()).toEqual(['Angular', 'React', 'Vue']);
});
});
Unit testing ensures that services behave as expected independently of components.
Step 11: Providing Services Manually in a Component
Although providedIn: 'root'
is preferred, you can also provide a service directly in a component.
@Component({
selector: 'app-custom',
templateUrl: './custom.component.html',
providers: [DataService]
})
export class CustomComponent {}
This creates a new instance of DataService
that is unique to this component and its children.
Step 12: Lazy Loading and Service Instances
Angular creates new instances of services for lazy-loaded modules if you use providedIn: 'any'
.
Example use case:
- You might want a separate instance of a configuration service for each feature module.
- Or separate state management per module.
This ensures services remain modular and independent between modules.
Step 13: Using Services for API Communication
Services are commonly used to interact with backend APIs.
Example:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private apiURL = 'https://jsonplaceholder.typicode.com/posts';
constructor(private http: HttpClient) {}
getPosts(): Observable<any[]> {
return this.http.get<any[]>(this.apiURL);
}
getPost(id: number): Observable<any> {
return this.http.get<any>(${this.apiURL}/${id}
);
}
createPost(post: any): Observable<any> {
return this.http.post<any>(this.apiURL, post);
}
}
This design keeps API logic separate from components, which focus only on displaying data.
Step 14: Common Mistakes to Avoid
- Creating services manually using
new
:
Always let Angular handle service creation using dependency injection.
Example of what not to do:this.dataService = new DataService();
- Forgetting to add
@Injectable
:
Without it, Angular can’t inject dependencies properly. - Registering the same service multiple times:
Doing so can cause unexpected multiple instances. - Ignoring error handling:
Always handle API or logic errors gracefully inside services.
Step 15: Summary of Key Points
- Services encapsulate reusable logic and can be injected anywhere.
- The
@Injectable
decorator allows Angular to inject dependencies into the service. providedIn: 'root'
makes a service globally available as a singleton.- Services make code modular, maintainable, and testable.
- Services can be used for:
- Data management
- API calls
- Shared state
- Utility functions
- Component communication
Step 16: Full Example Recap
data.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
getData() {
return ['Angular', 'React', 'Vue'];
}
}
app.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
frameworks: string[] = [];
constructor(private dataService: DataService) {}
ngOnInit() {
this.frameworks = this.dataService.getData();
}
}
app.component.html
<h1>Frontend Frameworks</h1>
<ul>
<li *ngFor="let framework of frameworks">{{ framework }}</li>
</ul>
This simple setup demonstrates how a service can provide data and how a component can consume it efficiently.
Step 17: Best Practices
- Keep services focused — one service for one responsibility.
- Use observables for asynchronous data.
- Avoid storing too much state in services unless needed.
- Always unsubscribe from service observables when components are destroyed.
- Use interfaces for type safety in data returned from services.
- Document each service method clearly.
- Write unit tests for all service logic.
Leave a Reply