Angular API Integration
Introduction
In modern web applications, integrating with external APIs is a fundamental requirement. Angular provides powerful tools for making HTTP requests and handling responses through its HttpClient
module. This guide will walk you through the process of integrating APIs in your Angular applications, from basic requests to advanced techniques.
API integration allows your Angular application to:
- Fetch data from server-side applications
- Send data to server endpoints
- Update and delete resources
- Authenticate users
- And much more!
Prerequisites
Before diving into API integration, make sure you have:
- Basic knowledge of Angular components and services
- Understanding of HTTP protocols (GET, POST, PUT, DELETE)
- A working Angular application
Setting Up HttpClient
The first step to integrating APIs in Angular is to set up the HttpClient
module.
1. Import HttpClientModule
Open your app.module.ts
file and add the following imports:
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 // Add this line
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
2. Create a Service for API Calls
It's a best practice to encapsulate all your API calls in services. Let's create a service for handling data from a fictional book API:
// books.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface Book {
id: number;
title: string;
author: string;
year: number;
}
@Injectable({
providedIn: 'root'
})
export class BooksService {
private apiUrl = 'https://api.example.com/books';
constructor(private http: HttpClient) { }
getBooks(): Observable<Book[]> {
return this.http.get<Book[]>(this.apiUrl);
}
getBook(id: number): Observable<Book> {
return this.http.get<Book>(`${this.apiUrl}/${id}`);
}
addBook(book: Omit<Book, 'id'>): Observable<Book> {
return this.http.post<Book>(this.apiUrl, book);
}
updateBook(book: Book): Observable<Book> {
return this.http.put<Book>(`${this.apiUrl}/${book.id}`, book);
}
deleteBook(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
Making HTTP Requests
Now that we have our service set up, let's see how to use it in a component.
GET Request Example
Here's how to fetch books in a component:
// books.component.ts
import { Component, OnInit } from '@angular/core';
import { BooksService } from './books.service';
@Component({
selector: 'app-books',
template: `
<div *ngIf="loading">Loading books...</div>
<div *ngIf="error">{{ error }}</div>
<ul *ngIf="books.length">
<li *ngFor="let book of books">
{{ book.title }} by {{ book.author }} ({{ book.year }})
</li>
</ul>
`
})
export class BooksComponent implements OnInit {
books: any[] = [];
loading = false;
error = '';
constructor(private booksService: BooksService) { }
ngOnInit(): void {
this.fetchBooks();
}
fetchBooks(): void {
this.loading = true;
this.booksService.getBooks()
.subscribe({
next: (data) => {
this.books = data;
this.loading = false;
},
error: (error) => {
this.error = 'Failed to fetch books!';
this.loading = false;
console.error('Error fetching books:', error);
}
});
}
}
POST Request Example
Let's see how to add a new book:
// add-book.component.ts
import { Component } from '@angular/core';
import { BooksService } from './books.service';
@Component({
selector: 'app-add-book',
template: `
<h2>Add New Book</h2>
<form (ngSubmit)="addBook()">
<div>
<label for="title">Title:</label>
<input type="text" id="title" [(ngModel)]="book.title" name="title" required>
</div>
<div>
<label for="author">Author:</label>
<input type="text" id="author" [(ngModel)]="book.author" name="author" required>
</div>
<div>
<label for="year">Year:</label>
<input type="number" id="year" [(ngModel)]="book.year" name="year" required>
</div>
<button type="submit">Add Book</button>
</form>
<div *ngIf="message">{{ message }}</div>
`
})
export class AddBookComponent {
book = {
title: '',
author: '',
year: 2023
};
message = '';
constructor(private booksService: BooksService) { }
addBook(): void {
this.booksService.addBook(this.book)
.subscribe({
next: (response) => {
this.message = `Book "${response.title}" added successfully with ID: ${response.id}`;
// Reset form
this.book = { title: '', author: '', year: 2023 };
},
error: (error) => {
this.message = 'Failed to add book!';
console.error('Error adding book:', error);
}
});
}
}
Error Handling
Proper error handling is crucial for a good user experience. Here's a more robust approach:
// enhanced-books.service.ts
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 EnhancedBooksService {
private apiUrl = 'https://api.example.com/books';
constructor(private http: HttpClient) { }
getBooks(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl)
.pipe(
retry(2), // Retry the request up to 2 times if it fails
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
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));
}
}
HTTP Headers and Request Configuration
Often you'll need to customize your HTTP requests with headers, parameters, or other configurations:
// auth-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthApiService {
private apiUrl = 'https://api.example.com';
constructor(private http: HttpClient) { }
login(username: string, password: string): Observable<any> {
const body = { username, password };
return this.http.post<any>(`${this.apiUrl}/login`, body);
}
getProtectedData(): Observable<any> {
// Get token from local storage
const token = localStorage.getItem('token');
// Set headers with authorization token
const headers = new HttpHeaders()
.set('Authorization', `Bearer ${token}`)
.set('Content-Type', 'application/json');
// Optional: Add query parameters
const params = new HttpParams()
.set('page', '1')
.set('limit', '10');
return this.http.get<any>(`${this.apiUrl}/data`, { headers, params });
}
}
Real-World Example: Weather App
Let's build a simple weather app that fetches data from a public weather API:
// weather.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
interface WeatherResponse {
main: {
temp: number;
humidity: number;
pressure: number;
};
weather: Array<{
description: string;
icon: string;
}>;
name: string;
}
interface WeatherData {
temperature: number;
humidity: number;
pressure: number;
description: string;
iconUrl: string;
cityName: string;
}
@Injectable({
providedIn: 'root'
})
export class WeatherService {
private apiKey = 'your_api_key_here'; // Replace with your OpenWeatherMap API key
private apiUrl = 'https://api.openweathermap.org/data/2.5/weather';
constructor(private http: HttpClient) { }
getWeather(city: string): Observable<WeatherData> {
const params = {
q: city,
appid: this.apiKey,
units: 'metric'
};
return this.http.get<WeatherResponse>(this.apiUrl, { params })
.pipe(
map(response => {
return {
temperature: response.main.temp,
humidity: response.main.humidity,
pressure: response.main.pressure,
description: response.weather[0].description,
iconUrl: `http://openweathermap.org/img/wn/${response.weather[0].icon}@2x.png`,
cityName: response.name
};
})
);
}
}
And now the component:
// weather.component.ts
import { Component } from '@angular/core';
import { WeatherService } from './weather.service';
@Component({
selector: 'app-weather',
template: `
<div class="weather-app">
<h2>Weather App</h2>
<div class="search-container">
<input type="text" [(ngModel)]="city" placeholder="Enter city name">
<button (click)="searchWeather()">Search</button>
</div>
<div class="weather-info" *ngIf="weatherData">
<h3>{{ weatherData.cityName }}</h3>
<div class="weather-details">
<img [src]="weatherData.iconUrl" alt="Weather icon">
<div class="temperature">{{ weatherData.temperature }}°C</div>
<div class="description">{{ weatherData.description }}</div>
</div>
<div class="additional-info">
<div>Humidity: {{ weatherData.humidity }}%</div>
<div>Pressure: {{ weatherData.pressure }} hPa</div>
</div>
</div>
<div class="error" *ngIf="error">{{ error }}</div>
<div class="loading" *ngIf="loading">Loading weather data...</div>
</div>
`,
styles: [`
.weather-app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.search-container {
display: flex;
margin-bottom: 20px;
}
.search-container input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
}
.search-container button {
padding: 10px 15px;
background-color: #0066cc;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.weather-info {
text-align: center;
}
.weather-details {
display: flex;
flex-direction: column;
align-items: center;
}
.temperature {
font-size: 2rem;
font-weight: bold;
}
.description {
text-transform: capitalize;
margin-bottom: 10px;
}
.additional-info {
display: flex;
justify-content: space-around;
margin-top: 20px;
}
.error {
color: red;
text-align: center;
}
.loading {
text-align: center;
color: #666;
}
`]
})
export class WeatherComponent {
city = '';
weatherData: any;
error = '';
loading = false;
constructor(private weatherService: WeatherService) { }
searchWeather(): void {
if (!this.city.trim()) {
this.error = 'Please enter a city name';
return;
}
this.loading = true;
this.error = '';
this.weatherData = null;
this.weatherService.getWeather(this.city)
.subscribe({
next: (data) => {
this.weatherData = data;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to fetch weather data. Please check the city name and try again.';
this.loading = false;
console.error('Weather API error:', err);
}
});
}
}
Handling API Responses
When dealing with APIs, you might need to transform or process the data before using it in your application. Angular's RxJS operators are perfect for this:
import { map, catchError } from 'rxjs/operators';
// Inside your service method
getFormattedBooks(): Observable<any[]> {
return this.http.get<any[]>('https://api.example.com/books')
.pipe(
map(books => books.map(book => ({
...book,
title: book.title.toUpperCase(),
publishedDate: new Date(book.year, 0, 1).toLocaleDateString()
}))),
catchError(error => {
console.error('Error in formatted books:', error);
return throwError(() => new Error('Failed to get formatted books'));
})
);
}
HTTP Interceptors
HTTP interceptors are a powerful feature in Angular that allows you to intercept and modify HTTP requests and responses globally. Here's an example of an interceptor that adds an authentication token to all outgoing requests:
// auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private router: Router) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = localStorage.getItem('token');
if (token) {
// Clone the request and add the authorization header
const authRequest = request.clone({
headers: request.headers.set('Authorization', `Bearer ${token}`)
});
// Pass the cloned request to the next handler
return next.handle(authRequest).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Token expired or invalid, redirect to login
localStorage.removeItem('token');
this.router.navigate(['/login']);
}
return throwError(() => error);
})
);
}
// If no token, pass the original request
return next.handle(request);
}
}
Register the interceptor in your app module:
// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
@NgModule({
// ...other module configuration
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
],
// ...
})
export class AppModule { }
Loading and Progress Indicators
It's a good practice to show loading indicators when fetching data. Here's how you can implement a simple loading state:
// Using async pipe with loading state
// products.component.ts
import { Component } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { finalize, switchMap } from 'rxjs/operators';
import { ProductsService } from './products.service';
@Component({
selector: 'app-products',
template: `
<h2>Products</h2>
<div class="loading-container">
<div class="loading-spinner" *ngIf="loading">Loading...</div>
</div>
<ul>
<li *ngFor="let product of products$ | async">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
<button (click)="refreshProducts()">Refresh</button>
`
})
export class ProductsComponent {
products$: Observable<any[]>;
loading = false;
private refreshSubject = new BehaviorSubject<void>(undefined);
constructor(private productsService: ProductsService) {
this.products$ = this.refreshSubject.pipe(
switchMap(() => {
this.loading = true;
return this.productsService.getProducts().pipe(
finalize(() => this.loading = false)
);
})
);
}
refreshProducts(): void {
this.refreshSubject.next();
}
ngOnInit(): void {
this.refreshProducts();
}
}
Summary
In this guide, you've learned:
- How to set up
HttpClient
in your Angular application - Making different types of HTTP requests (GET, POST, PUT, DELETE)
- Handling API responses and errors
- Using interceptors to modify requests globally
- Implementing loading indicators
- Building a real-world example with a weather API
API integration is a crucial part of modern web applications, and Angular's HttpClient
module provides a powerful and flexible way to handle these interactions. By following the patterns and practices outlined in this guide, you can build robust applications that effectively communicate with external services.
Additional Resources
- Angular HttpClient Official Documentation
- RxJS Library Documentation
- JSON Server - A full fake REST API for testing
- Postman - API Development Environment for testing APIs
Exercises
- Basic Exercise: Create a service that fetches data from the JSONPlaceholder API and displays a list of posts.
- Intermediate Exercise: Build a CRUD application that allows users to view, add, edit, and delete items from an API.
- Advanced Exercise: Create a paginated data table that loads data from an API with sorting and filtering capabilities.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)