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.
// 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.
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
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:
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
:
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:
// 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:
// 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:
// todo.model.ts
export interface Todo {
id?: number;
title: string;
completed: boolean;
userId: number;
}
Create a service to handle API operations:
// 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:
// 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:
- Create dedicated service classes for API interactions rather than making HTTP requests directly in components
- Define interfaces for API responses to leverage TypeScript's type checking
- Use interceptors for cross-cutting concerns like authentication or logging
- Implement proper error handling using RxJS operators
- Consider caching responses for performance improvement
- Use environment files to store API URLs for different environments
- 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
- Official Angular HttpClient Documentation
- RxJS Documentation
- JSONPlaceholder - A free fake REST API for testing
- Angular University HttpClient Course
Exercises
- Extend the Todo application to include filtering by completion status
- Add pagination support to the Todo list
- Create a loading spinner component that shows during HTTP requests
- Implement a caching mechanism for GET requests to improve performance
- 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! :)