← Back to Tutorials Chapter 9

HTTP & Services

HttpClient Overview

Angular's HttpClient service, part of @angular/common/http, provides a simplified API for making HTTP requests. It returns typed Observables, supports request/response interception, and includes features like progress events and JSONP.

Setting Up HttpClientModule

Import HttpClientModule into your root or feature module:

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [HttpClientModule],
  // ...
})
export class AppModule { }

For standalone apps, use provideHttpClient() in the providers array:

// main.ts or app.config.ts
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient()]
};

Making HTTP Requests

GET Request

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

interface Product {
  id: number;
  name: string;
  price: number;
}

@Injectable({ providedIn: 'root' })
export class ProductService {
  private apiUrl = 'https://api.example.com/products';

  constructor(private http: HttpClient) { }

  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(this.apiUrl);
  }
}

POST Request

createProduct(product: Product): Observable<Product> {
  return this.http.post<Product>(this.apiUrl, product);
}

PUT Request

updateProduct(id: number, product: Product): Observable<Product> {
  return this.http.put<Product>(`${this.apiUrl}/${id}`, product);
}

DELETE Request

deleteProduct(id: number): Observable<void> {
  return this.http.delete<void>(`${this.apiUrl}/${id}`);
}

HttpParams

Add query parameters to requests:

import { HttpParams } from '@angular/common/http';

searchProducts(query: string, page: number): Observable<Product[]> {
  const params = new HttpParams()
    .set('q', query)
    .set('page', page.toString())
    .set('limit', '20');

  return this.http.get<Product[]>(this.apiUrl, { params });
}

HttpHeaders

Set custom headers for requests:

import { HttpHeaders } from '@angular/common/http';

getHeaders(): HttpHeaders {
  return new HttpHeaders({
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${this.auth.getToken()}`,
    'X-Custom-Header': 'custom-value'
  });
}

getSecuredData(): Observable<any> {
  return this.http.get(this.apiUrl, { headers: this.getHeaders() });
}

Subscribing to Observables

HttpClient methods return cold Observables. The request is made only when you subscribe:

this.productService.getProducts().subscribe({
  next: (products) => this.products = products,
  error: (err) => this.errorMessage = 'Failed to load products',
  complete: () => this.loading = false
});

Error Handling

Use RxJS operators like catchError and retry for robust error handling:

import { catchError, retry, throwError } from 'rxjs';

getProducts(): Observable<Product[]> {
  return this.http.get<Product[]>(this.apiUrl).pipe(
    retry(2),  // Retry failed requests up to 2 times
    catchError(this.handleError)
  );
}

private handleError(error: HttpErrorResponse) {
  let errorMsg = 'An unknown error occurred';
  if (error.error instanceof ErrorEvent) {
    errorMsg = `Client error: ${error.error.message}`;
  } else {
    errorMsg = `Server error: ${error.status} - ${error.message}`;
  }
  console.error(errorMsg);
  return throwError(() => new Error(errorMsg));
}

Interceptors

Interceptors examine and transform HTTP requests and responses globally. They are useful for adding auth tokens, logging, caching, or error handling.

Auth Interceptor

import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) { }

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const token = this.auth.getToken();
    const authReq = token
      ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
      : req;
    return next.handle(authReq);
  }
}

// Provide the interceptor
providers: [
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]

Logging Interceptor

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const start = Date.now();
    return next.handle(req).pipe(
      tap(() => console.log(`${req.method} ${req.url} took ${Date.now() - start}ms`))
    );
  }
}

File Upload

Upload files using FormData with POST:

uploadFile(file: File): Observable<any> {
  const formData = new FormData();
  formData.append('file', file, file.name);
  return this.http.post('/api/upload', formData, {
    reportProgress: true,
    observe: 'events'
  });
}

Subscribe with progress tracking:

this.uploadService.uploadFile(file).subscribe(event => {
  if (event.type === HttpEventType.UploadProgress) {
    const percent = Math.round(100 * event.loaded / event.total);
    this.progress = percent;
  } else if (event.type === HttpEventType.Response) {
    console.log('Upload complete', event.body);
  }
});

JSONP Support

For cross-origin requests that don't support CORS, use HttpClient.jsonp():

this.http.jsonp('https://api.example.com/callback?format=jsonp', 'callback')
  .subscribe(data => console.log(data));
Angular HTTP client services

Summary

You learned how to use Angular's HttpClient for full CRUD operations, configure HttpParams and HttpHeaders, handle errors with catchError and retry, implement request/response interceptors for auth and logging, and handle file uploads with progress tracking. These HTTP patterns are essential for any real-world Angular application.

Exercise 9: Create a TodoService with full CRUD operations against JSONPlaceholder (https://jsonplaceholder.typicode.com/todos). Implement a logging interceptor that logs all request URLs and durations. Add a retry of 1 for failed requests. Build a TodoComponent that displays todos, marks them as complete (PUT), adds new todos (POST), and deletes them (DELETE) with error handling using catchError.