Skip to main content

Angular HTTP Client

Introduction

Angular provides a powerful HTTP client module that allows your applications to communicate with backend services over the HTTP protocol. The HttpClient service is Angular's built-in mechanism for making HTTP requests and handling responses, replacing the older Http service from previous versions.

In modern web applications, fetching data from servers, submitting form data, and interacting with REST APIs are fundamental operations. Angular's HttpClient simplifies these tasks while providing features like typed responses, request/response interception, error handling, and testability.

In this tutorial, we'll explore how to use Angular's HttpClient to perform various HTTP operations and integrate with backend services.

Setting Up HttpClient

Before you can use HttpClient, you need to import the HttpClientModule into your application module.

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

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

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

Once you've imported the module, you can inject the HttpClient service into any component or service where you need to make HTTP requests.

Basic HTTP Requests

GET Request

The most common HTTP request is a GET request, which retrieves data from a server.

typescript
// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

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

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

constructor(private http: HttpClient) { }

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

getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
}

Now, let's use this service in a component:

typescript
// user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from '../data.service';

@Component({
selector: 'app-user-list',
template: `
<h2>User List</h2>
<div *ngIf="loading">Loading...</div>
<ul *ngIf="!loading">
<li *ngFor="let user of users">{{ user.name }} ({{ user.email }})</li>
</ul>
<div *ngIf="error">{{ error }}</div>
`
})
export class UserListComponent implements OnInit {
users: any[] = [];
loading = false;
error = '';

constructor(private dataService: DataService) { }

ngOnInit(): void {
this.loading = true;
this.dataService.getUsers().subscribe({
next: (data) => {
this.users = data;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load users: ' + err.message;
this.loading = false;
}
});
}
}

POST Request

To create a new resource on the server, you'll typically use a POST request:

typescript
// In data.service.ts, add this method:
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}

And use it in a component:

typescript
// user-form.component.ts
import { Component } from '@angular/core';
import { DataService } from '../data.service';

@Component({
selector: 'app-user-form',
template: `
<h2>Create User</h2>
<form (ngSubmit)="onSubmit()">
<div>
<label for="name">Name:</label>
<input type="text" id="name" [(ngModel)]="user.name" name="name" required>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" [(ngModel)]="user.email" name="email" required>
</div>
<button type="submit">Create User</button>
</form>
<div *ngIf="message">{{ message }}</div>
`
})
export class UserFormComponent {
user = { name: '', email: '' };
message = '';

constructor(private dataService: DataService) { }

onSubmit() {
this.dataService.createUser(this.user).subscribe({
next: (response) => {
this.message = `User created with ID: ${response.id}`;
this.user = { name: '', email: '' }; // Reset form
},
error: (err) => {
this.message = 'Failed to create user: ' + err.message;
}
});
}
}

PUT and PATCH Requests

PUT is used to update an entire resource, while PATCH is used for partial updates:

typescript
// In data.service.ts, add these methods:
updateUser(id: number, user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${id}`, user);
}

patchUser(id: number, partialUser: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}`, partialUser);
}

DELETE Request

To delete a resource:

typescript
// In data.service.ts, add this method:
deleteUser(id: number): Observable<any> {
return this.http.delete(`${this.apiUrl}/${id}`);
}

Working with HTTP Headers

You can customize the HTTP request headers using the HttpHeaders class:

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

// ...

getProtectedResource(): Observable<any> {
const token = localStorage.getItem('auth_token');
const headers = new HttpHeaders({
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
});

return this.http.get<any>('https://api.example.com/protected-resource', { headers });
}

Request Parameters

You can add URL parameters to your requests using the HttpParams class:

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

// ...

searchUsers(term: string, page: number = 1): Observable<User[]> {
let params = new HttpParams()
.set('q', term)
.set('page', page.toString());

return this.http.get<User[]>(`${this.apiUrl}/search`, { params });
}

This will generate a URL like: https://jsonplaceholder.typicode.com/users/search?q=john&page=1

Error Handling

To handle HTTP errors more effectively, you can use RxJS operators like catchError:

typescript
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';

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

constructor(private http: HttpClient) { }

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

private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// Client-side error
console.error('Client error:', error.error.message);
} else {
// Server-side error
console.error(
`Server error: ${error.status}, ` +
`message: ${error.error}`
);
}
// Return a user-facing error message
return throwError(() => new Error('Something went wrong. Please try again later.'));
}
}

Interceptors

HTTP interceptors allow you to intercept and modify HTTP requests and responses globally across your application. This is useful for tasks like adding authentication headers to all requests or handling errors consistently.

First, create an interceptor:

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

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Get the auth token from storage
const authToken = localStorage.getItem('auth_token');

// Clone the request and add the auth header
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${authToken || ''}`)
});

// Pass the cloned request with the auth header to the next handler
return next.handle(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Handle unauthorized errors (e.g., redirect to login)
console.log('Unauthorized access - redirecting to login');
}
return throwError(() => error);
})
);
}
}

Then register it in your app module:

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

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

Real-World Example: Building a Todo Application

Let's put everything together in a more complete example - a simple todo application that interacts with a REST API:

First, define the Todo interface:

typescript
// todo.model.ts
export interface Todo {
id?: number;
title: string;
completed: boolean;
userId: number;
}

Create a service to handle API operations:

typescript
// todo.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Todo } from './todo.model';

@Injectable({
providedIn: 'root'
})
export class TodoService {
private apiUrl = 'https://jsonplaceholder.typicode.com/todos';

constructor(private http: HttpClient) { }

// Get all todos or filter by user ID
getTodos(userId?: number): Observable<Todo[]> {
let params = new HttpParams();

if (userId) {
params = params.set('userId', userId.toString());
}

return this.http.get<Todo[]>(this.apiUrl, { params })
.pipe(catchError(this.handleError));
}

// Get a single todo by ID
getTodoById(id: number): Observable<Todo> {
return this.http.get<Todo>(`${this.apiUrl}/${id}`)
.pipe(catchError(this.handleError));
}

// Create a new todo
createTodo(todo: Omit<Todo, 'id'>): Observable<Todo> {
return this.http.post<Todo>(this.apiUrl, todo)
.pipe(catchError(this.handleError));
}

// Update a todo
updateTodo(id: number, todo: Todo): Observable<Todo> {
return this.http.put<Todo>(`${this.apiUrl}/${id}`, todo)
.pipe(catchError(this.handleError));
}

// Toggle todo completion status
toggleCompletion(id: number, completed: boolean): Observable<Todo> {
return this.http.patch<Todo>(`${this.apiUrl}/${id}`, { completed })
.pipe(catchError(this.handleError));
}

// Delete a todo
deleteTodo(id: number): Observable<any> {
return this.http.delete(`${this.apiUrl}/${id}`)
.pipe(catchError(this.handleError));
}

// Error handling
private handleError(error: HttpErrorResponse) {
let errorMessage = 'An unknown error occurred!';

if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}

console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}

Create a component to display and manage todos:

typescript
// todos.component.ts
import { Component, OnInit } from '@angular/core';
import { TodoService } from './todo.service';
import { Todo } from './todo.model';

@Component({
selector: 'app-todos',
template: `
<div class="container">
<h1>Todo List</h1>

<!-- Todo creation form -->
<div class="todo-form">
<input type="text" [(ngModel)]="newTodo.title" placeholder="What needs to be done?">
<button (click)="addTodo()">Add Todo</button>
</div>

<!-- Loading state -->
<div *ngIf="loading" class="loading">Loading todos...</div>

<!-- Error state -->
<div *ngIf="error" class="error">{{ error }}</div>

<!-- Todo list -->
<div class="todo-list" *ngIf="!loading && !error">
<div *ngFor="let todo of todos" class="todo-item">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo)"
>
<span [class.completed]="todo.completed">{{ todo.title }}</span>
<button (click)="deleteTodo(todo.id)">Delete</button>
</div>

<div *ngIf="todos.length === 0" class="no-todos">
No todos found. Create one above!
</div>
</div>
</div>
`,
styles: [`
.todo-item {
margin: 8px 0;
padding: 8px;
border: 1px solid #ddd;
display: flex;
align-items: center;
}
.completed {
text-decoration: line-through;
color: #888;
}
.todo-item span {
flex-grow: 1;
margin: 0 10px;
}
.loading, .error, .no-todos {
padding: 20px;
text-align: center;
}
.error {
color: red;
}
`]
})
export class TodosComponent implements OnInit {
todos: Todo[] = [];
loading = false;
error = '';
newTodo: Omit<Todo, 'id'> = {
title: '',
completed: false,
userId: 1 // In a real app, this would be the current user's ID
};

constructor(private todoService: TodoService) { }

ngOnInit(): void {
this.loadTodos();
}

loadTodos(): void {
this.loading = true;
this.todoService.getTodos().subscribe({
next: (todos) => {
this.todos = todos.slice(0, 10); // Limit to 10 items for this example
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load todos: ' + err.message;
this.loading = false;
}
});
}

addTodo(): void {
if (!this.newTodo.title.trim()) {
return; // Don't add empty todos
}

this.todoService.createTodo(this.newTodo).subscribe({
next: (todo) => {
this.todos.unshift(todo); // Add to beginning of array
this.newTodo.title = ''; // Reset input
},
error: (err) => {
this.error = 'Failed to add todo: ' + err.message;
}
});
}

toggleTodo(todo: Todo): void {
if (!todo.id) return;

const updatedTodo = { ...todo, completed: !todo.completed };

this.todoService.toggleCompletion(todo.id, !todo.completed).subscribe({
next: () => {
todo.completed = !todo.completed; // Update the local state
},
error: (err) => {
this.error = 'Failed to update todo: ' + err.message;
}
});
}

deleteTodo(id?: number): void {
if (!id) return;

this.todoService.deleteTodo(id).subscribe({
next: () => {
this.todos = this.todos.filter(todo => todo.id !== id);
},
error: (err) => {
this.error = 'Failed to delete todo: ' + err.message;
}
});
}
}

Best Practices

When working with HttpClient in Angular, follow these best practices:

  1. Create dedicated service classes for API interactions rather than making HTTP requests directly in components
  2. Define interfaces for API responses to leverage TypeScript's type checking
  3. Use interceptors for cross-cutting concerns like authentication or logging
  4. Implement proper error handling using RxJS operators
  5. Consider caching responses for performance improvement
  6. Use environment files to store API URLs for different environments
  7. Test your HTTP requests using Angular's HttpClientTestingModule

Summary

The Angular HttpClient is a powerful tool for communicating with servers in your Angular applications. In this tutorial, we've covered:

  • Setting up HttpClient in your application
  • Making basic HTTP requests (GET, POST, PUT, PATCH, DELETE)
  • Working with headers and request parameters
  • Handling errors
  • Using interceptors for global request/response handling
  • Building a complete Todo application with HttpClient

With these skills, you can build robust Angular applications that effectively communicate with backend services and APIs.

Additional Resources

Exercises

  1. Extend the Todo application to include filtering by completion status
  2. Add pagination support to the Todo list
  3. Create a loading spinner component that shows during HTTP requests
  4. Implement a caching mechanism for GET requests to improve performance
  5. Create a custom error handling service that displays user-friendly error messages


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)