Chapter 10: HTTP and APIs
Almost every real-world application needs to talk to a backend — to fetch data, submit forms, authenticate users, or save changes. Angular provides a powerful and flexible way to handle HTTP communication via the HttpClient service.
In this chapter, we’ll cover:
- Setting up
HttpClientModule. - Making GET, POST, PUT, DELETE requests.
- Handling errors.
- Adding headers and query parameters.
- Using interceptors for authentication and logging.
- Combining HTTP with signals for reactive state.
10.1 Setting Up HttpClient
To start using HTTP in Angular, import the module:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient()]
});
👉 With standalone APIs, you use provideHttpClient() instead of importing HttpClientModule.
10.2 Making Requests
All requests are made through the HttpClient service, which Angular injects.
Example: Fetching Users (GET Request)
Service:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl);
}
}
Component:
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`
})
export class UserListComponent implements OnInit {
users: any[] = [];
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUsers().subscribe(data => (this.users = data));
}
}
10.3 Sending Data (POST, PUT, DELETE)
POST Example (Creating a User)
createUser(user: any): Observable<any> {
return this.http.post(this.apiUrl, user);
}
PUT Example (Updating a User)
updateUser(id: number, user: any): Observable<any> {
return this.http.put(`${this.apiUrl}/${id}`, user);
}
DELETE Example (Removing a User)
deleteUser(id: number): Observable<any> {
return this.http.delete(`${this.apiUrl}/${id}`);
}
10.4 Adding Headers and Query Params
Headers Example:
this.http.get(url, {
headers: { Authorization: 'Bearer my-token' }
});
Query Params Example:
this.http.get(url, {
params: { page: '1', limit: '10' }
});
10.5 Error Handling
Always handle errors gracefully using RxJS catchError.
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
getUsers(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl).pipe(
catchError(err => {
console.error('Error fetching users', err);
return throwError(() => new Error('Failed to fetch users'));
})
);
}
10.6 Http Interceptors
Interceptors let you modify requests/responses globally — great for authentication, logging, or error handling.
Example: Auth Interceptor
import { Injectable } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authReq = req.clone({
setHeaders: { Authorization: `Bearer fake-jwt-token` }
});
return next(authReq);
};
Register it in your main.ts:
import { withInterceptors } from '@angular/common/http';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([authInterceptor]))]
});
10.7 HTTP + Signals
With Angular 16+, you can combine HttpClient with signals for simpler state management.
import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class PostService {
posts = signal<any[]>([]);
loading = signal(false);
constructor(private http: HttpClient) {}
loadPosts() {
this.loading.set(true);
this.http.get<any[]>('https://jsonplaceholder.typicode.com/posts')
.subscribe(data => {
this.posts.set(data);
this.loading.set(false);
});
}
}
Component:
@Component({
selector: 'app-posts',
standalone: true,
template: `
<button (click)="service.loadPosts()">Load Posts</button>
<p *ngIf="service.loading()">Loading...</p>
<ul>
<li *ngFor="let post of service.posts()">{{ post.title }}</li>
</ul>
`
})
export class PostsComponent {
constructor(public service: PostService) {}
}
Now the template reacts automatically to changes in posts() and loading().
10.8 Using Async/Await with HttpClient
By default, Angular’s HttpClient methods (get, post, etc.) return Observables. This gives you powerful operators, streaming, and cancellation — but sometimes you want the simplicity of async/await.
Angular provides utilities in rxjs to convert Observables to Promises:
firstValueFrom()– resolves with the first emitted value.lastValueFrom()– resolves with the last emitted value.
Example: Fetching Data with async/await
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'app-async-posts',
standalone: true,
template: `
<button (click)="loadPosts()">Load Posts</button>
<ul>
<li *ngFor="let post of posts">{{ post.title }}</li>
</ul>
`
})
export class AsyncPostsComponent {
posts: any[] = [];
constructor(private http: HttpClient) {}
async loadPosts() {
try {
const response = await firstValueFrom(
this.http.get<any[]>('https://jsonplaceholder.typicode.com/posts')
);
this.posts = response;
} catch (err) {
console.error('Error fetching posts', err);
}
}
}
✅ Here:
- We wrapped the
http.getObservable infirstValueFrom(). - Now we can use
awaitto get the data like a Promise. - Errors are caught with
try/catch.
Example: POST with Async/Await
async createPost(post: any) {
return await firstValueFrom(
this.http.post('https://jsonplaceholder.typicode.com/posts', post)
);
}
When to Use Async/Await vs Observables
-
Use async/await when:
- You want simple, one-off calls (e.g., fetching data once).
- You’re writing code that already uses async/await patterns.
-
Use Observables when:
- You need streams of data (live updates, multiple emissions).
- You need operators like
map,switchMap,debounceTime. - You want cancellation (unsubscribe).
Side-by-Side Comparison
| Aspect | Observable (subscribe) | Async/Await (firstValueFrom) |
|---|---|---|
| Style | Reactive, functional | Imperative, synchronous-looking |
| Multiple emissions | ✅ Supported | 🚫 Only first/last value |
| Cancellation | ✅ Can unsubscribe | 🚫 No built-in cancellation |
| Error handling | error callback | try/catch |
| Best for | Streams, continuous updates | One-off requests, simple flows |
10.9 Async/Await with Signals (loading + error + data)
This pattern keeps all request state in a small “store-like” service using signals. Components stay tiny and declarative.
Service (signals + async/await):
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
export interface Post {
id?: number;
title: string;
body: string;
}
@Injectable({ providedIn: 'root' })
export class PostsStore {
// reactive state
readonly posts = signal<Post[]>([]);
readonly loading = signal(false);
readonly error = signal<string | null>(null);
// derived state
readonly count = computed(() => this.posts().length);
readonly hasError = computed(() => this.error() !== null);
constructor(private http: HttpClient) {}
async loadPosts() {
this.loading.set(true);
this.error.set(null);
try {
const data = await firstValueFrom(
this.http.get<Post[]>('https://jsonplaceholder.typicode.com/posts')
);
this.posts.set(data);
} catch (e: any) {
this.error.set(e?.message ?? 'Failed to load posts');
} finally {
this.loading.set(false);
}
}
async addPost(post: Post) {
this.loading.set(true);
this.error.set(null);
try {
const created = await firstValueFrom(
this.http.post<Post>('https://jsonplaceholder.typicode.com/posts', post)
);
// append immutably so OnPush/change detection + signals stay happy
this.posts.update(list => [created, ...list]);
} catch (e: any) {
this.error.set(e?.message ?? 'Failed to create post');
} finally {
this.loading.set(false);
}
}
// handy helper to clear an error from the UI
clearError() {
this.error.set(null);
}
}
Component (tiny, purely declarative):
import { Component } from '@angular/core';
import { PostsStore } from './posts.store';
@Component({
selector: 'app-posts',
standalone: true,
template: `
<button (click)="store.loadPosts()" [disabled]="store.loading()">Reload</button>
<form (ngSubmit)="create()" style="margin:1rem 0;">
<input placeholder="Title" [(ngModel)]="title" name="title">
<input placeholder="Body" [(ngModel)]="body" name="body">
<button type="submit" [disabled]="store.loading()">Add</button>
</form>
<p *ngIf="store.loading()">Loading…</p>
<p *ngIf="store.hasError()">
❌ {{ store.error() }}
<button (click)="store.clearError()">dismiss</button>
</p>
<p *ngIf="!store.loading()">Total: {{ store.count() }}</p>
<ul>
<li *ngFor="let p of store.posts()">
<strong>{{ p.title }}</strong> — {{ p.body }}
</li>
</ul>
`
})
export class PostsComponent {
title = '';
body = '';
constructor(public store: PostsStore) {}
async create() {
await this.store.addPost({ title: this.title, body: this.body });
this.title = '';
this.body = '';
}
}
Why this works well
- Signals make UI reads trivial:
store.loading()/store.error()/store.posts(). - Async/await keeps request code linear and easy to reason about.
- Computed (
count) demonstrates derived state that auto-updates with no extra wiring. - Errors & loading are centralized in the service, not sprinkled through components.
Tip: For sequences (load → then add),
awaitnaturally serializes operations. For parallel calls,Promise.all()works fine withfirstValueFrom(...)-wrapped requests.
10.10 Best Practice:
- Don’t replace all Observables with async/await. Instead, use both where they fit best. Async/await is great for one-shot requests; Observables shine for reactive, continuous data flows.
- Centralize API logic in services, not components.
- Always handle errors (network failures happen).
- Use interceptors for authentication, logging, and retry strategies.
- Combine signals with HTTP for reactive UI updates.
- Keep URLs/configs in environment files for easy switching (dev, prod).
10.11 Chapter Summary
- Angular’s
HttpClientreturns Observables by default. - You can use async/await with
firstValueFrom()orlastValueFrom()to simplify code. - Observables remain more powerful for streams, operators, and cancellation.