cyberangles guide

Angular HTTP Client: Making API Calls

Angular’s `HttpClient` is a module in `@angular/common/http` that provides a simplified interface for making HTTP requests (GET, POST, PUT, DELETE, etc.) to backend APIs. It replaces the older, deprecated `Http` module and offers several improvements: - **Automatic JSON Parsing**: Responses are automatically parsed into JSON, eliminating the need for manual `res.json()` calls. - **Type Safety**: Supports TypeScript interfaces, ensuring type-checked responses. - **Interceptors**: Allows intercepting requests/responses for cross-cutting concerns like authentication, logging, or error handling. - **Reactive Programming**: Returns RxJS Observables, enabling powerful operations like mapping, filtering, and error handling. Whether you’re building a small app or a large enterprise solution, `HttpClient` is the standard for API communication in Angular.

In modern web applications, interacting with backend APIs is a fundamental requirement. Whether you’re fetching user data, submitting forms, or updating records, your frontend needs a reliable way to communicate with servers. Angular, a popular TypeScript-based framework, simplifies this process with its built-in HttpClient module. This powerful toolstreamlines making HTTP requests, handling responses, and managing errors—all while leveraging Angular’s reactive programming paradigm with RxJS.

In this blog, we’ll dive deep into Angular’s HttpClient, covering everything from setup and basic API calls to advanced topics like interceptors and error handling. By the end, you’ll be equipped to build robust, production-ready API integrations in your Angular apps.

Table of Contents

  1. Introduction to Angular HttpClient
  2. Setting Up HttpClient
  3. Basic HTTP Methods
  4. Working with Headers
  5. Error Handling
  6. Observables and RxJS Integration
  7. HTTP Interceptors
  8. Advanced Topics
  9. Best Practices
  10. Conclusion
  11. References

Setting Up HttpClient

Before using HttpClient, you need to import HttpClientModule in your Angular application. This module provides the necessary services and dependencies.

Step 1: Import HttpClientModule

Open your root module (typically app.module.ts) and add HttpClientModule to the imports array:

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; // Import HttpClientModule

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule], // Add to imports
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Step 2: Inject HttpClient into Services

Angular recommends encapsulating API logic in services (not components) for reusability and separation of concerns. To use HttpClient, inject it into your service via the constructor.

Example: Create a UserService to handle user-related API calls:

// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; // Import HttpClient

@Injectable({
  providedIn: 'root' // Service is available application-wide
})
export class UserService {
  // Inject HttpClient via constructor
  constructor(private http: HttpClient) {}
}

Now your service is ready to make API calls!

Basic HTTP Methods

HttpClient provides methods for all standard HTTP verbs. Let’s explore the most common ones with examples using the JSONPlaceholder mock API (a free service for testing API calls).

GET: Fetching Data

Use http.get() to retrieve data from an API.

Example: Fetch a List of Users

In UserService, add a method to fetch users:

// user.service.ts
import { Observable } from 'rxjs';
import { User } from './user.model'; // Define a User interface

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private apiUrl = 'https://jsonplaceholder.typicode.com/users'; // API URL

  constructor(private http: HttpClient) {}

  // Fetch all users
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl); // Returns Observable<User[]>
  }

  // Fetch a single user by ID
  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
}

Using the Service in a Component

Subscribe to the Observable in a component to receive the data:

// user.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
import { User } from './user.model';

@Component({
  selector: 'app-user',
  template: `
    <div *ngIf="users.length">
      <h2>Users</h2>
      <ul>
        <li *ngFor="let user of users">{{ user.name }} ({{ user.email }})</li>
      </ul>
    </div>
  `
})
export class UserComponent implements OnInit {
  users: User[] = [];

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    // Subscribe to getUsers()
    this.userService.getUsers().subscribe({
      next: (data) => this.users = data, // Handle successful response
      error: (err) => console.error('Error fetching users:', err) // Handle errors
    });
  }
}

Note: Always handle errors in the subscribe method (or use catchError in the service, covered later).

POST: Sending Data

Use http.post(url, body) to send data to the API (e.g., creating a new resource).

Example: Create a New User

Add a method to UserService to create a user:

// user.service.ts
createUser(user: Omit<User, 'id'>): Observable<User> {
  // Omit 'id' since the API will generate it
  return this.http.post<User>(this.apiUrl, user);
}

Using createUser in a Component

// user.component.ts
createNewUser(): void {
  const newUser = { name: 'John Doe', email: '[email protected]' };
  this.userService.createUser(newUser).subscribe({
    next: (createdUser) => console.log('User created:', createdUser),
    error: (err) => console.error('Error creating user:', err)
  });
}

The API will return the created user with an auto-generated id.

PUT: Updating Data

Use http.put(url, body) to fully update an existing resource (replaces the entire resource).

Example: Update a User

// user.service.ts
updateUser(id: number, updatedUser: User): Observable<User> {
  return this.http.put<User>(`${this.apiUrl}/${id}`, updatedUser);
}

Usage in Component:

updateExistingUser(): void {
  const updatedUser = { id: 1, name: 'Jane Doe', email: '[email protected]' };
  this.userService.updateUser(1, updatedUser).subscribe({
    next: (user) => console.log('User updated:', user),
    error: (err) => console.error('Error updating user:', err)
  });
}

DELETE: Removing Data

Use http.delete(url) to delete a resource.

Example: Delete a User

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

Usage in Component:

deleteExistingUser(): void {
  this.userService.deleteUser(1).subscribe({
    next: () => console.log('User deleted successfully'),
    error: (err) => console.error('Error deleting user:', err)
  });
}

Working with Headers

Headers provide metadata about requests/responses (e.g., authentication tokens, content type). Use HttpHeaders to configure headers for requests.

Example: Adding Headers

// user.service.ts
import { HttpHeaders } from '@angular/common/http';

// Create headers
const headers = new HttpHeaders({
  'Content-Type': 'application/json', // Required for JSON bodies
  'Authorization': 'Bearer YOUR_AUTH_TOKEN' // Example: Add auth token
});

// Use headers in a request
getUsersWithHeaders(): Observable<User[]> {
  return this.http.get<User[]>(this.apiUrl, { headers });
}

Shorthand for Headers: You can also pass headers as an object:

this.http.get(this.apiUrl, {
  headers: { 'Content-Type': 'application/json' }
});

Common Headers

  • Content-Type: application/json: Required when sending JSON data in POST/PUT requests.
  • Authorization: Bearer <token>: For JWT authentication.
  • Accept: application/json: Specifies the response format.

Error Handling

API calls can fail (e.g., network issues, 404 Not Found). Use RxJS operators like catchError to handle errors gracefully.

Handling Errors in the Service

Centralize error handling in the service using catchError and throwError:

// user.service.ts
import { catchError, throwError } from 'rxjs';

getUsers(): Observable<User[]> {
  return this.http.get<User[]>(this.apiUrl).pipe(
    catchError((error) => {
      console.error('API Error:', error);
      // Custom error handling (e.g., log to service, show toast)
      return throwError(() => new Error('Failed to fetch users. Please try again later.'));
    })
  );
}

Handling Errors in the Component

The component can then handle the rethrown error in the subscribe method:

this.userService.getUsers().subscribe({
  next: (data) => this.users = data,
  error: (err) => this.showError(err.message) // Display error to user
});

Common Error Status Codes

  • 400 Bad Request: Invalid data sent.
  • 401 Unauthorized: Missing/invalid authentication.
  • 404 Not Found: Resource doesn’t exist.
  • 500 Internal Server Error: Server-side issue.

Observables and RxJS Integration

HttpClient methods return RxJS Observables, which represent a stream of data. This allows you to use RxJS operators to transform, filter, or combine responses.

Key Operators for API Calls

  • map: Transform the response data.

    import { map } from 'rxjs/operators';
    
    getUsernames(): Observable<string[]> {
      return this.http.get<User[]>(this.apiUrl).pipe(
        map(users => users.map(user => user.name)) // Extract only names
      );
    }
  • filter: Filter responses based on a condition.

    import { filter } from 'rxjs/operators';
    
    getActiveUsers(): Observable<User[]> {
      return this.http.get<User[]>(this.apiUrl).pipe(
        filter(users => users.length > 0) // Only emit if users exist
      );
    }
  • tap: Perform side effects (e.g., logging) without modifying the stream.

    import { tap } from 'rxjs/operators';
    
    getUsersWithLogging(): Observable<User[]> {
      return this.http.get<User[]>(this.apiUrl).pipe(
        tap(users => console.log('Fetched users:', users))
      );
    }

Unsubscribing

Always unsubscribe from Observables to prevent memory leaks. Use:

  • The async pipe in templates (auto-unsubscribes):

    <div *ngFor="let user of users$ | async">{{ user.name }}</div>

    (In the component: users$ = this.userService.getUsers();)

  • takeUntil with a destroy subject:

    import { Subject, takeUntil } from 'rxjs';
    
    private destroy$ = new Subject<void>();
    
    ngOnInit(): void {
      this.userService.getUsers().pipe(
        takeUntil(this.destroy$) // Unsubscribe when destroy$ emits
      ).subscribe(data => this.users = data);
    }
    
    ngOnDestroy(): void {
      this.destroy$.next();
      this.destroy$.complete();
    }

HTTP Interceptors

Interceptors are middleware that intercept and modify HTTP requests/responses globally. Use cases include:

  • Adding auth tokens to all requests.
  • Logging requests/responses.
  • Handling errors globally.
  • Adding headers (e.g., Content-Type).

Creating an Interceptor

An interceptor implements the HttpInterceptor interface and overrides the intercept method.

Example 1: Auth Interceptor (Add JWT Token)

Add a bearer token to all outgoing requests:

// auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor() {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    // Get token from local storage (or auth service)
    const token = localStorage.getItem('authToken');

    if (token) {
      // Clone the request and add the token
      const authReq = request.clone({
        headers: request.headers.set('Authorization', `Bearer ${token}`)
      });
      return next.handle(authReq); // Pass modified request
    }

    return next.handle(request); // Pass original request if no token
  }
}

Example 2: Logging Interceptor

Log request/response details:

// logging.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpResponse
} from '@angular/common/http';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    console.log('Outgoing Request:', request);

    return next.handle(request).pipe(
      tap({
        next: (event) => {
          if (event instanceof HttpResponse) {
            console.log('Incoming Response:', event);
          }
        },
        error: (err) => console.log('Request Error:', err)
      })
    );
  }
}

Providing Interceptors

Register interceptors in your root module using the HTTP_INTERCEPTORS token:

// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
import { LoggingInterceptor } from './logging.interceptor';

@NgModule({
  // ...
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
  ]
})
export class AppModule {}

Note: multi: true allows multiple interceptors to run in sequence.

Advanced Topics

Caching Responses

Reduce API calls by caching responses using shareReplay:

// user.service.ts
import { shareReplay } from 'rxjs/operators';

private usersCache$: Observable<User[]> | null = null;

getUsersCached(): Observable<User[]> {
  if (!this.usersCache$) {
    this.usersCache$ = this.http.get<User[]>(this.apiUrl).pipe(
      shareReplay(1) // Cache the last emission
    );
  }
  return this.usersCache$;
}

Tracking Request Progress

Use reportProgress: true and HttpEvent to track upload/download progress:

// file-upload.service.ts
import { HttpClient, HttpRequest, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class FileUploadService {
  constructor(private http: HttpClient) {}

  uploadFile(file: File): Observable<HttpEvent<any>> {
    const formData = new FormData();
    formData.append('file', file);

    const req = new HttpRequest('POST', '/api/upload', formData, {
      reportProgress: true, // Enable progress tracking
      responseType: 'json'
    });

    return this.http.request(req); // Returns HttpEvent stream
  }
}

In the component, track progress:

uploadFile(file: File): void {
  this.fileUploadService.uploadFile(file).subscribe({
    next: (event) => {
      if (event.type === HttpEventType.UploadProgress) {
        const percent = Math.round((100 * event.loaded) / (event.total || 1));
        console.log(`Upload progress: ${percent}%`);
      } else if (event instanceof HttpResponse) {
        console.log('File uploaded:', event.body);
      }
    }
  });
}

Best Practices

  1. Keep API Logic in Services: Never make API calls directly in components—use services for reusability.
  2. Use Environment Variables: Store API URLs in environment.ts for easy configuration across environments:
    // environment.ts
    export const environment = {
      production: false,
      apiUrl: 'https://jsonplaceholder.typicode.com'
    };
  3. Handle Loading States: Show spinners/loaders while waiting for responses.
  4. Validate Data: Use TypeScript interfaces and libraries like zod to validate API responses.
  5. Avoid Hardcoded Headers: Use interceptors to add headers globally.

Conclusion

Angular’s HttpClient is a powerful tool for interacting with backend APIs, offering simplicity, type safety, and integration with RxJS. By mastering its features—from basic HTTP methods to interceptors and error handling—you can build robust, maintainable Angular applications that communicate seamlessly with servers.

Remember to follow best practices like centralizing API logic in services, handling errors gracefully, and using interceptors for cross-cutting concerns. With HttpClient, you’ll streamline your API workflows and focus on building great user experiences.

References