Introduction to Services
A service in Angular is a plain TypeScript class that encapsulates logic you want to reuse and share — business rules, data access, caching, logging, or shared state — kept deliberately separate from any single component. Components should be thin: they render the view and react to user input, while services do the heavy lifting behind the scenes. This separation keeps your UI code small and testable, and it lets many components depend on the same piece of logic without duplicating it.
Why services exist
Components are tied to a template and a slice of the screen. If you put data-fetching, formatting, or state management directly inside a component, that code becomes hard to reuse and hard to test, and it tends to leak across components through clumsy @Input/@Output chains. A service solves this by being a single, long-lived object that any component (or other service) can ask for.
The core responsibilities of services typically fall into a few buckets:
| Responsibility | Example |
|---|---|
| Data access | Calling an HTTP API and returning typed results |
| Business logic | Validating an order, calculating a price |
| Shared state | Holding the current user or a shopping cart |
| Cross-cutting concerns | Logging, analytics, feature flags |
Defining a service
A service is just a class. The @Injectable decorator marks it as a candidate for Angular’s dependency injection (DI) system, and providedIn: 'root' registers it as an application-wide singleton — Angular creates exactly one instance and shares it everywhere.
import { Injectable } from '@angular/core';
export interface Product {
id: number;
name: string;
price: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private items: Product[] = [];
add(product: Product): void {
this.items.push(product);
}
remove(id: number): void {
this.items = this.items.filter((p) => p.id !== id);
}
total(): number {
return this.items.reduce((sum, p) => sum + p.price, 0);
}
list(): readonly Product[] {
return this.items;
}
}
The class knows nothing about templates, change detection, or which component uses it. That ignorance is the point — it can be used anywhere and tested in isolation.
providedIn: 'root'makes the service tree-shakable: if nothing injectsCartService, the bundler can drop it entirely. Prefer it over registering services in a module or componentprovidersarray unless you specifically need a narrower scope.
Consuming a service in a component
Modern Angular components retrieve services with the inject() function. You call it at field-initializer position and store the result; Angular resolves the dependency from the injector hierarchy and hands you the shared instance.
import { Component, inject } from '@angular/core';
import { CartService, Product } from './cart.service';
@Component({
selector: 'app-cart',
standalone: true,
template: `
@if (cart.list().length > 0) {
<ul>
@for (item of cart.list(); track item.id) {
<li>{{ item.name }} — {{ item.price | currency }}</li>
}
</ul>
<p>Total: {{ cart.total() | currency }}</p>
} @else {
<p>Your cart is empty.</p>
}
<button (click)="addSample()">Add sample item</button>
`,
})
export class CartComponent {
protected readonly cart = inject(CartService);
private nextId = 1;
addSample(): void {
const product: Product = { id: this.nextId++, name: 'Widget', price: 9.99 };
this.cart.add(product);
}
}
Because both CartComponent and any other component inject the same CartService instance, adding an item in one place is visible everywhere that reads the cart.
Services depending on other services
Services compose. A higher-level service can inject() lower-level ones — for example a checkout service that uses the cart and a logger.
import { Injectable, inject } from '@angular/core';
import { CartService } from './cart.service';
@Injectable({ providedIn: 'root' })
export class LoggerService {
log(message: string): void {
console.log(`[checkout] ${message}`);
}
}
@Injectable({ providedIn: 'root' })
export class CheckoutService {
private readonly cart = inject(CartService);
private readonly logger = inject(LoggerService);
checkout(): number {
const total = this.cart.total();
this.logger.log(`Charging customer ${total.toFixed(2)}`);
return total;
}
}
Output:
[checkout] Charging customer 19.98
Angular wires these dependencies for you. You never call new CartService() yourself — the framework constructs the graph and reuses the singleton instances.
Best practices
- Keep components focused on the view and delegate logic, data access, and state to services.
- Annotate every service with
@Injectableand preferprovidedIn: 'root'for app-wide singletons that are tree-shakable. - Use
inject()over constructor parameter injection in modern code — it reads cleanly and works in field initializers and functions. - Expose narrow, intention-revealing methods (
add,total) rather than leaking mutable internal state. - Never instantiate services with
new— let DI provide them so dependencies and lifetimes stay consistent. - Keep services free of UI concerns so they remain easy to unit test in isolation.