Angular's HttpClient is the standard way to communicate with APIs. Here's how to use it correctly in Angular 21 — typed requests, interceptors, error handling, loading states, and the modern toSignal() pattern.

Setup

Register HttpClient in main.ts:

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { provideHttpClient, withInterceptors } from '@angular/common/http'
import { AppComponent } from './app/app.component'

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient() // or provideHttpClient(withInterceptors([...]))
  ]
})

Basic Requests

import { inject } from '@angular/core'
import { HttpClient } from '@angular/common/http'

interface Template {
  id: number
  name: string
  framework: string
  price: number
}

export class TemplateService {
  private http = inject(HttpClient)
  private apiUrl = 'https://api.lettstartdesign.com'

  // GET — fetch list
  getTemplates() {
    return this.http.get<Template[]>(`${this.apiUrl}/templates`)
  }

  // GET — fetch single item
  getTemplate(id: number) {
    return this.http.get<Template>(`${this.apiUrl}/templates/${id}`)
  }

  // POST — create
  createTemplate(template: Partial<Template>) {
    return this.http.post<Template>(`${this.apiUrl}/templates`, template)
  }

  // PUT — full update
  updateTemplate(id: number, template: Template) {
    return this.http.put<Template>(`${this.apiUrl}/templates/${id}`, template)
  }

  // PATCH — partial update
  patchTemplate(id: number, changes: Partial<Template>) {
    return this.http.patch<Template>(`${this.apiUrl}/templates/${id}`, changes)
  }

  // DELETE
  deleteTemplate(id: number) {
    return this.http.delete<void>(`${this.apiUrl}/templates/${id}`)
  }
}

Loading State + Error Handling in a Component

Using signals alongside RxJS for clean async state management:

import { Component, signal, inject } from '@angular/core'
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
import { NgIf, NgFor, CurrencyPipe } from '@angular/common'

interface Template {
  id: number
  name: string
  framework: string
  price: number
}

@Component({
  selector: 'app-templates',
  standalone: true,
  imports: [NgIf, NgFor, CurrencyPipe],
  template: `
    <div class="container py-4">
      <h1 class="fw-bold mb-4">Templates</h1>

      @if (loading()) {
        <div class="d-flex justify-content-center py-5">
          <div class="spinner-border" style="color:#fd4766;"></div>
        </div>
      }

      @if (error()) {
        <div class="alert alert-danger d-flex align-items-center gap-2">
          <span>⚠️</span>
          <span>{{ error() }}</span>
          <button class="btn btn-sm btn-outline-danger ms-auto" (click)="load()">Retry</button>
        </div>
      }

      @if (!loading() && !error()) {
        <div class="row g-3">
          @for (t of templates(); track t.id) {
            <div class="col-md-4">
              <div class="card border-0 shadow-sm h-100">
                <div class="card-body p-4">
                  <span class="badge mb-2" style="background:#fd4766;color:#fff;">{{ t.framework }}</span>
                  <h5 class="fw-bold">{{ t.name }}</h5>
                  <p class="fw-bold mt-auto mb-0" style="color:#fd4766;">{{ t.price | currency }}</p>
                </div>
              </div>
            </div>
          }
        </div>
      }
    </div>
  `
})
export class TemplatesComponent {
  private http = inject(HttpClient)

  templates = signal<Template[]>([])
  loading   = signal(false)
  error     = signal<string | null>(null)

  ngOnInit() {
    this.load()
  }

  load() {
    this.loading.set(true)
    this.error.set(null)

    this.http.get<Template[]>('/api/templates').subscribe({
      next: (data) => {
        this.templates.set(data)
        this.loading.set(false)
      },
      error: (err: HttpErrorResponse) => {
        this.error.set(this.parseError(err))
        this.loading.set(false)
      }
    })
  }

  private parseError(err: HttpErrorResponse): string {
    if (err.status === 0)   return 'Network error — check your connection.'
    if (err.status === 401) return 'Not authorised — please log in.'
    if (err.status === 403) return 'You do not have permission to view this.'
    if (err.status === 404) return 'Templates not found.'
    if (err.status >= 500)  return 'Server error — please try again later.'
    return err.message || 'An unexpected error occurred.'
  }
}

Using toSignal() — Cleanest Pattern

toSignal() from @angular/core/rxjs-interop converts an observable into a signal in one line:

import { Component, inject, signal } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { toSignal } from '@angular/core/rxjs-interop'
import { switchMap } from 'rxjs/operators'

@Component({
  selector: 'app-template-detail',
  standalone: true,
  template: `
    @if (template()) {
      <h1>{{ template()!.name }}</h1>
    } @else {
      <div class="spinner-border" style="color:#fd4766;"></div>
    }
  `
})
export class TemplateDetailComponent {
  private http = inject(HttpClient)

  // Observable converted to signal — no subscription needed
  template = toSignal(
    this.http.get<{ id: number; name: string }>('/api/templates/1'),
    { initialValue: null }
  )
}

Query Parameters

import { HttpParams } from '@angular/common/http'

getTemplates(framework?: string, page = 1) {
  let params = new HttpParams()
    .set('page', page)
    .set('limit', 12)

  if (framework) {
    params = params.set('framework', framework)
  }

  return this.http.get<Template[]>('/api/templates', { params })
  // Produces: /api/templates?page=1&limit=12&framework=Angular
}

HTTP Interceptors

Interceptors are function-based in Angular 21:

// src/app/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http'
import { inject } from '@angular/core'
import { AuthService } from '../services/auth.service'

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth  = inject(AuthService)
  const token = auth.getToken()

  if (!token) return next(req)

  const authReq = req.clone({
    headers: req.headers.set('Authorization', `Bearer ${token}`)
  })

  return next(authReq)
}
// src/app/interceptors/error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'
import { inject } from '@angular/core'
import { Router } from '@angular/router'
import { catchError, throwError } from 'rxjs'

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router)

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        router.navigate(['/login'])
      }
      return throwError(() => error)
    })
  )
}

Register in main.ts:

provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))

Auto-Cancel on Component Destroy

import { takeUntilDestroyed } from '@angular/core/rxjs-interop'

export class TemplatesComponent {
  private http = inject(HttpClient)

  ngOnInit() {
    this.http.get<Template[]>('/api/templates')
      .pipe(takeUntilDestroyed()) // auto-cancels when component destroys
      .subscribe(data => this.templates.set(data))
  }
}

Request Headers and Options

import { HttpHeaders } from '@angular/common/http'

// Custom headers on a single request
const headers = new HttpHeaders({
  'Content-Type': 'application/json',
  'X-Custom-Header': 'my-value'
})

this.http.post('/api/templates', body, { headers })

// Observe full response (not just body)
this.http.get('/api/templates', { observe: 'response' }).subscribe(response => {
  console.log(response.status)    // 200
  console.log(response.headers.get('X-Total-Count'))  // pagination total
  console.log(response.body)      // the actual data
})

// Track upload progress
this.http.post('/api/upload', formData, {
  reportProgress: true,
  observe: 'events'
}).subscribe(event => {
  if (event.type === HttpEventType.UploadProgress && event.total) {
    const percent = Math.round(100 * event.loaded / event.total)
    this.uploadProgress.set(percent)
  }
})

Frequently Asked Questions

Inject HttpClient using inject(HttpClient) or constructor injection, then call this.http.get<T>(url), .post<T>(url, body), .put<T>(url, body), or .delete<T>(url). These return Observables — subscribe to them or use toSignal() to convert to a signal.
Create an HTTP interceptor function that reads the auth token and adds an Authorization header using request.clone({ headers: request.headers.set('Authorization', 'Bearer ' + token) }). Register it with withInterceptors([authInterceptor]) in provideHttpClient().
Use RxJS catchError in a pipe: this.http.get(url).pipe(catchError(error => { if (error.status === 401) // redirect; return throwError(() => error) })). For global error handling, use an HTTP interceptor that catches errors across all requests.
Use the takeUntilDestroyed() operator with a DestroyRef to auto-cancel when the component destroys, or use a Subject with takeUntil() for manual cancellation. You can also use HttpContext to pass cancellation tokens.

Need a Full Bootstrap 5 Admin Dashboard?

Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off your first purchase.

Related Components