Services and Dependency Injection
@Injectable services, constructor and inject()-based DI, and singleton services with providedIn: root.
Why services exist
A component's job is to present data and handle user interaction — not to own business logic, fetch data directly, or hold state that multiple unrelated components need to share. Angular's convention for that other code is a service: a plain TypeScript class dedicated to one concern (fetching users, managing a shopping cart, logging), which components depend on rather than implementing that logic themselves.
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class UserService {
private users = [
{ id: 1, name: "Ada Lovelace" },
{ id: 2, name: "Grace Hopper" },
];
getUsers() {
return this.users;
}
getUserById(id: number) {
return this.users.find((u) => u.id === id);
}
}
@Injectable() marks a class as available to Angular's dependency injection system. providedIn: "root" registers it as a singleton — one single shared instance exists for the entire application, and Angular creates it lazily the first time something actually asks for it.
Dependency injection, conceptually
Dependency injection (DI) means a class declares what it needs in its constructor, and something else — here, the Angular framework itself — is responsible for actually providing (constructing and passing in) that dependency. The class receiving it never calls new UserService() itself:
import { Component, inject } from "@angular/core";
import { UserService } from "./user.service";
@Component({
selector: "app-user-list",
standalone: true,
template: `
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`,
})
export class UserListComponent {
private userService = inject(UserService);
users = this.userService.getUsers();
}
The same thing written with the older, still very common constructor-injection style:
import { Component } from "@angular/core";
import { UserService } from "./user.service";
@Component({
selector: "app-user-list",
standalone: true,
template: `
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`,
})
export class UserListComponent {
users: { id: number; name: string }[];
constructor(private userService: UserService) {
this.users = this.userService.getUsers();
}
}
Both forms achieve the same thing: UserListComponent never constructs a UserService itself. It declares "I need a UserService," and Angular's injector resolves that dependency — creating the singleton instance if it doesn't already exist, or handing over the existing one if it does — and supplies it automatically. The newer inject() function (used inside a class field initializer, as shown first) is the more modern style; the constructor-parameter style is still extremely common in existing Angular code and works identically.
Why this matters: testability and decoupling
The real payoff of dependency injection is that UserListComponent depends only on UserService's public interface — getUsers(), getUserById() — not on how it's implemented. In a test, you can supply a fake UserService (with hardcoded test data and no real HTTP calls) instead of the real one, without touching UserListComponent's code at all:
import { TestBed } from "@angular/core/testing";
import { UserService } from "./user.service";
import { UserListComponent } from "./user-list.component";
const fakeUserService = {
getUsers: () => [{ id: 1, name: "Test User" }],
};
TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [{ provide: UserService, useValue: fakeUserService }],
});
This is the same principle behind "program to an interface, not an implementation" in any object-oriented language — Angular's DI system just makes it the default, structurally encouraged way of writing components, rather than something you have to remember to do manually.
A more realistic example: a service used by a component
// product.service.ts
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
export interface Product {
id: number;
name: string;
price: number;
}
@Injectable({
providedIn: "root",
})
export class ProductService {
private apiUrl = "/api/products";
constructor(private http: HttpClient) {}
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl);
}
}
// product-list.component.ts
import { Component, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ProductService } from "./product.service";
@Component({
selector: "app-product-list",
standalone: true,
imports: [CommonModule],
template: `
<ul>
<li *ngFor="let product of products$ | async">
{{ product.name }} — \${{ product.price }}
</li>
</ul>
`,
})
export class ProductListComponent {
private productService = inject(ProductService);
products$ = this.productService.getProducts();
}
ProductService itself depends on HttpClient — Angular's own built-in service for making HTTP requests — injected the same way ProductListComponent injects ProductService. Dependency injection chains naturally: a service can depend on other services, and Angular resolves the whole graph automatically. HttpClient and the products$ | async pattern are covered in full on the next page.
Common mistakes
- Constructing a service manually with
new UserService()instead of injecting it — this bypasses Angular's DI system entirely, silently creating a second, separate instance instead of sharing the intended singleton. - Putting business logic or HTTP calls directly inside a component instead of a service — makes the logic untestable in isolation and impossible to reuse from another component.
- Forgetting
providedIn: "root"(or otherwise registering the service with a provider) — without it, Angular has no configured way to create the service when something requests it.