Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.get Observable in firstValueFrom().
  • Now we can use await to 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

AspectObservable (subscribe)Async/Await (firstValueFrom)
StyleReactive, functionalImperative, synchronous-looking
Multiple emissions✅ Supported🚫 Only first/last value
Cancellation✅ Can unsubscribe🚫 No built-in cancellation
Error handlingerror callbacktry/catch
Best forStreams, continuous updatesOne-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), await naturally serializes operations. For parallel calls, Promise.all() works fine with firstValueFrom(...)-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 HttpClient returns Observables by default.
  • You can use async/await with firstValueFrom() or lastValueFrom() to simplify code.
  • Observables remain more powerful for streams, operators, and cancellation.