Services and dependency injection
Injectable services, the inject() function, providers and tokens, plus an HTTP layer with interceptors and typed responses.
Writing and consuming a service
// cart.store.ts
import { Injectable, signal, computed } from '@angular/core'
@Injectable({ providedIn: 'root' }) // one instance for the whole application
export class CartStore {
private readonly items = signal<Item[]>([])
readonly count = computed(() => this.items().length)
readonly total = computed(() => this.items().reduce((t, i) => t + i.price, 0))
add(item: Item) {
this.items.update(list => [...list, item])
}
remove(id: string) {
this.items.update(list => list.filter(i => i.id !== id))
}
}import { Component, inject } from '@angular/core'
import { CartStore } from './cart.store'
@Component({ selector: 'app-cart', template: '{{ cart.total() }}' })
export class CartComponent {
readonly cart = inject(CartStore) // resolved from the injector, no constructor ceremony
}providedIn: 'root'registers a tree-shakable singleton that is created on first injection — the default for application-wide services.inject()works in constructors and field initialisers, and is the only way to get a dependency inside a functional guard, interceptor or resolver.providedIn: 'any'and component-levelprovidersgive each lazy module or component instance its own copy.- Keep a service free of template concerns: it should expose state and operations, not formatted strings for one screen.
Providers, tokens and overrides
// app.config.ts
import { ApplicationConfig, inject } from '@angular/core'
import { provideRouter } from '@angular/router'
import { provideHttpClient, withInterceptors } from '@angular/common/http'
import { routes } from './app.routes'
export const API_BASE = new InjectionToken<string>('API_BASE')
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
{ provide: API_BASE, useValue: 'https://api.example.com' }
]
}// a component-level override, useful in tests and previews
@Component({
selector: 'app-preview',
providers: [{ provide: API_BASE, useValue: 'https://staging.example.com' }],
template: '...'
})
export class PreviewComponent {}
// in a unit test
TestBed.configureTestingModule({
providers: [{ provide: CartStore, useValue: fakeCart }]
})| Provider form | Use when |
|---|---|
useClass | Swapping one implementation for another |
useValue | Config constants, fakes and mocks |
useFactory | The value must be computed or depends on other services |
useExisting | Two tokens should resolve to the same instance |
InjectionToken | A non-class dependency, especially configuration |
⚠️
A component that declares its own
providers gets a private instance for every instance of that component — a common reason two parts of a screen disagree about state that a shared service should own.HTTP with interceptors
import { Injectable, inject } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { API_BASE } from '../app.config'
export interface Guide { id: string; title: string; tags: string[] }
@Injectable({ providedIn: 'root' })
export class GuideService {
private http = inject(HttpClient)
private base = inject(API_BASE)
list() {
return this.http.get<Guide[]>(this.base + '/guides')
}
create(payload: { title: string }) {
return this.http.post<Guide>(this.base + '/guides', payload)
}
}// auth.interceptor.ts — functional interceptor, registered in app.config.ts
import { HttpInterceptorFn } from '@angular/common/http'
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token')
if (!token || req.url.startsWith('https://cdn.')) return next(req)
return next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } }))
}HttpClientreturns cold observables: nothing happens until something subscribes, which is why templates use theasyncpipe or aresourcewrapper.- Interceptors are the right place for auth headers, retries, correlation ids and global error reporting — one implementation instead of one per call site.
- Requests must be cloned before modification; a request object is immutable.
- Type the response generically and validate it at the boundary if the API is not under your control; a TypeScript assertion is not a runtime check.
FAQ
Service or shared component state?
A service when state must outlive one component or be shared between siblings — that is why stores are services. Component-local state stays in signals on the component itself.
How do I test a component that uses HttpClient?
Provide the testing backend instead of the real one:
provideHttpClientTesting() plus HttpTestingController to assert the requests and flush canned responses.Related
Components and templates Routing and forms
Last refreshed 2026-09-18.