Docs
/
Angular
Chapter 5
05 — Services & HTTP Client
Services in Angular
A service is a class with a focused purpose — data access, business logic, state, or utility functions. Components should be thin (presentation only); services handle the heavy lifting.
@Injectable({ providedIn: 'root' })
export class UserService {
private apiUrl = inject(API_URL);
private http = inject(HttpClient);
getAll(): Observable<User[]> {
return this.http.get<User[]>(`${this.apiUrl}/users`);
}
}
Setting Up HttpClient
app.config.ts
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, errorInterceptor]),
withFetch(), // Use fetch API instead of XMLHttpRequest
),
],
};
CRUD with HttpClient
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
private baseUrl = `${environment.apiUrl}/products`;
// GET all (with query params)
getAll(page = 1, limit = 10, search?: string): Observable<PaginatedResponse<Product>> {
let params = new HttpParams()
.set('page', page)
.set('limit', limit);
if (search) params = params.set('search', search);
return this.http.get<PaginatedResponse<Product>>(this.baseUrl, { params });
}
// GET one
getById(id: string): Observable<Product> {
return this.http.get<Product>(`${this.baseUrl}/${id}`);
}
// POST
create(data: CreateProductDto): Observable<Product> {
return this.http.post<Product>(this.baseUrl, data);
}
// PUT (full replace)
update(id: string, data: Product): Observable<Product> {
return this.http.put<Product>(`${this.baseUrl}/${id}`, data);
}
// PATCH (partial update)
patch(id: string, data: Partial<Product>): Observable<Product> {
return this.http.patch<Product>(`${this.baseUrl}/${id}`, data);
}
// DELETE
delete(id: string): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`);
}
}
Using Services in Components
With async Pipe (Preferred)
@Component({
template: `
@if (products$ | async; as products) {
@for (p of products; track p.id) {
<app-product-card [product]="p" />
}
} @else {
<app-spinner />
}
`,
})
export class ProductListComponent {
private productService = inject(ProductService);
products$ = this.productService.getAll();
}
With Manual Subscribe
export class ProductListComponent implements OnInit {
private productService = inject(ProductService);
private destroyRef = inject(DestroyRef);
products: Product[] = [];
loading = true;
error: string | null = null;
ngOnInit() {
this.productService.getAll()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (res) => { this.products = res.data; this.loading = false; },
error: (err) => { this.error = err.message; this.loading = false; },
});
}
}
HTTP Interceptors (Functional — Angular 15+)
Interceptors modify every HTTP request/response globally.
Auth Interceptor — Attach JWT Token
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
const cloned = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(cloned);
}
return next(req);
};
Error Interceptor — Global Error Handling
import { HttpInterceptorFn } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const toast = inject(ToastService);
return next(req).pipe(
catchError(error => {
switch (error.status) {
case 401:
router.navigate(['/login']);
break;
case 403:
toast.error('You do not have permission');
break;
case 404:
toast.error('Resource not found');
break;
case 500:
toast.error('Server error. Please try again later.');
break;
default:
toast.error(error.error?.message ?? 'An error occurred');
}
return throwError(() => error);
}),
);
};
Loading Interceptor — Global Spinner
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loading = inject(LoadingService);
loading.show();
return next(req).pipe(
finalize(() => loading.hide()),
);
};
Retry Interceptor
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
retry({
count: 2,
delay: (error, retryCount) => {
if (error.status >= 400 && error.status < 500) {
return throwError(() => error); // Don't retry 4xx
}
return timer(1000 * retryCount); // Exponential backoff
},
}),
);
};
Register Interceptors
// app.config.ts
provideHttpClient(
withInterceptors([
authInterceptor,
loadingInterceptor,
retryInterceptor,
errorInterceptor, // Keep error handler last
]),
),
File Upload
upload(file: File): Observable<HttpEvent<UploadResponse>> {
const formData = new FormData();
formData.append('file', file, file.name);
return this.http.post<UploadResponse>(`${this.baseUrl}/upload`, formData, {
reportProgress: true,
observe: 'events',
});
}
// Usage — track progress
this.productService.upload(file).subscribe(event => {
if (event.type === HttpEventType.UploadProgress && event.total) {
this.progress = Math.round(100 * event.loaded / event.total);
}
if (event.type === HttpEventType.Response) {
console.log('Upload complete:', event.body);
}
});
Caching with Service
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
private cache$ = new Map<string, Observable<Product>>();
getById(id: string): Observable<Product> {
if (!this.cache$.has(id)) {
this.cache$.set(
id,
this.http.get<Product>(`/api/products/${id}`).pipe(
shareReplay(1), // Cache the last emission
),
);
}
return this.cache$.get(id)!;
}
invalidateCache(id: string) {
this.cache$.delete(id);
}
}
Type-Safe API Service Pattern
// models/api.model.ts
export interface ApiResponse<T> {
data: T;
message: string;
statusCode: number;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
totalPages: number;
}
// Generic base service
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
private baseUrl = inject(API_URL);
get<T>(path: string, params?: HttpParams): Observable<T> {
return this.http.get<T>(`${this.baseUrl}${path}`, { params });
}
post<T>(path: string, body: unknown): Observable<T> {
return this.http.post<T>(`${this.baseUrl}${path}`, body);
}
put<T>(path: string, body: unknown): Observable<T> {
return this.http.put<T>(`${this.baseUrl}${path}`, body);
}
patch<T>(path: string, body: unknown): Observable<T> {
return this.http.patch<T>(`${this.baseUrl}${path}`, body);
}
delete<T>(path: string): Observable<T> {
return this.http.delete<T>(`${this.baseUrl}${path}`);
}
}
Key Takeaways
- Keep components thin — move HTTP, state, and business logic into services
- Use
provideHttpClient(withInterceptors([...]))to register functional interceptors - Use
asyncpipe in templates to auto-manage subscriptions - Functional interceptors (Angular 15+) are simpler than class-based ones
- Order of interceptors matters — auth first, error handler last
- Use
shareReplay(1)for simple caching; aMap<string, Observable>for per-entity caching - Always type your API responses —
this.http.get<Product>(url)— never useany