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
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.