Skip to main content

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:

typescript
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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
// 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:

typescript
// 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

Exercises

  1. Basic Exercise: Create a service that fetches data from the JSONPlaceholder API and displays a list of posts.
  2. Intermediate Exercise: Build a CRUD application that allows users to view, add, edit, and delete items from an API.
  3. 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! :)