JSON in Angular: HttpClient, Interfaces, and RxJS
Last updated:
Angular's HttpClient is the standard way to fetch and send JSON data in Angular applications. Unlike the browser's native fetch, HttpClient automatically parses JSON responses, integrates with RxJS observables, and provides built-in support for interceptors, typed responses, and error handling. This guide covers everything from initial setup to advanced RxJS patterns and Angular 17+ standalone APIs.
Setting Up HttpClientModule
Before using HttpClient, you must provide it in your application. In Angular 17+ standalone apps, use provideHttpClient() in bootstrapApplication. In module-based apps, import HttpClientModule in AppModule.
// Angular 17+ standalone (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(
// optional: add functional interceptors here
// withInterceptors([authInterceptor, loggingInterceptor])
),
],
})
// --- OR --- Module-based apps (app.module.ts)
import { NgModule } from '@angular/core'
import { HttpClientModule } from '@angular/common/http'
import { BrowserModule } from '@angular/platform-browser'
import { AppComponent } from './app.component'
@NgModule({
imports: [BrowserModule, HttpClientModule],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class AppModule {}GET JSON with HttpClient
HttpClient.get<T>(url) sends a GET request and returns an Observable<T> that emits the parsed JSON body. Angular sets Accept: application/json by default and deserializes the response automatically.
// product.service.ts
import { Injectable, inject } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'
export interface Product {
id: number
name: string
price: number
category: string
}
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient) // Angular 17+ inject()
private baseUrl = '/api'
// GET all products
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(`${this.baseUrl}/products`)
}
// GET single product by ID
getProduct(id: number): Observable<Product> {
return this.http.get<Product>(`${this.baseUrl}/products/${id}`)
}
}
// product-list.component.ts — using subscribe()
import { Component, OnInit, inject } from '@angular/core'
import { ProductService, Product } from './product.service'
@Component({
selector: 'app-product-list',
standalone: true,
template: `
<ul>
<li *ngFor="let p of products">{{ p.name }} — {{ p.price | currency }}</li>
</ul>
`,
})
export class ProductListComponent implements OnInit {
products: Product[] = []
private productService = inject(ProductService)
ngOnInit() {
this.productService.getProducts().subscribe({
next: (products) => (this.products = products),
error: (err) => console.error('Failed to load products', err),
})
}
}Prefer the async pipe over manual subscription — it automatically unsubscribes when the component is destroyed, preventing memory leaks:
// Using async pipe — no manual subscribe/unsubscribe needed
import { Component, inject } from '@angular/core'
import { AsyncPipe, NgFor, CurrencyPipe } from '@angular/common'
import { ProductService } from './product.service'
@Component({
selector: 'app-product-list',
standalone: true,
imports: [AsyncPipe, NgFor, CurrencyPipe],
template: `
@if (products$ | async; as products) {
<ul>
@for (product of products; track product.id) {
<li>{{ product.name }} — {{ product.price | currency }}</li>
}
</ul>
}
`,
})
export class ProductListComponent {
products$ = inject(ProductService).getProducts()
}POST JSON Data
HttpClient.post<T>(url, body) serializes the body to JSON and sets Content-Type: application/json automatically. You do not need to call JSON.stringify.
// product.service.ts — POST, PUT, PATCH, DELETE
import { Injectable, inject } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'
import { Product } from './product.model'
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient)
private baseUrl = '/api/products'
// POST — create a new product
createProduct(data: Omit<Product, 'id'>): Observable<Product> {
return this.http.post<Product>(this.baseUrl, data)
// Angular sets: Content-Type: application/json
// and serializes data with JSON.stringify automatically
}
// PUT — full replacement
replaceProduct(id: number, data: Omit<Product, 'id'>): Observable<Product> {
return this.http.put<Product>(`${this.baseUrl}/${id}`, data)
}
// PATCH — partial update
updateProduct(id: number, data: Partial<Product>): Observable<Product> {
return this.http.patch<Product>(`${this.baseUrl}/${id}`, data)
}
// DELETE
deleteProduct(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`)
}
}
// In a component:
this.productService.createProduct({ name: 'Widget', price: 9.99, category: 'tools' })
.subscribe({
next: (created) => console.log('Created:', created.id),
error: (err) => console.error(err),
})Typing Responses with Interfaces
TypeScript interfaces describe the shape of JSON responses. Passing the interface as a generic type to HttpClient methods gives you full type safety and IDE autocompletion.
// models/product.model.ts
// Simple entity interface
export interface Product {
id: number
name: string
price: number
category: string
createdAt: string // ISO 8601 — JSON has no Date type
}
// Paginated API response wrapper
export interface PagedResponse<T> {
data: T[]
total: number
page: number
pageSize: number
}
// Nested relationships
export interface OrderDetail {
id: number
status: 'pending' | 'shipped' | 'delivered' | 'cancelled'
items: Array<{
productId: number
quantity: number
unitPrice: number
}>
customer: {
id: number
name: string
email: string
}
}
// Usage in service
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
@Injectable({ providedIn: 'root' })
export class OrderService {
private http = inject(HttpClient)
// Typed paginated response
getOrders(page: number): Observable<PagedResponse<OrderDetail>> {
return this.http.get<PagedResponse<OrderDetail>>(
`/api/orders?page=${page}`
)
}
// Extract nested data with map()
getOrderItems(orderId: number) {
return this.http.get<OrderDetail>(`/api/orders/${orderId}`).pipe(
map((order) => order.items)
// returns Observable<Array<{ productId, quantity, unitPrice }>>
)
}
}RxJS Operators: map, catchError, switchMap
RxJS operators transform and compose Observables returned by HttpClient. Use pipe() to chain them.
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import {
Observable, Subject, EMPTY, throwError, of
} from 'rxjs'
import {
map, catchError, switchMap, debounceTime,
distinctUntilChanged, retry, tap, shareReplay
} from 'rxjs/operators'
@Injectable({ providedIn: 'root' })
export class SearchService {
private http = inject(HttpClient)
// map — transform response shape
getProductNames(): Observable<string[]> {
return this.http.get<{ id: number; name: string }[]>('/api/products').pipe(
map((products) => products.map((p) => p.name))
)
}
// catchError — handle errors gracefully
getProductsSafe(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products').pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 404) return of([]) // empty array on 404
if (err.status === 0) return of([]) // network error
return throwError(() => err) // rethrow others
})
)
}
// retry — automatic retry on transient failures
getProductsWithRetry(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products').pipe(
retry({ count: 3, delay: 1000 }) // retry up to 3 times, 1s apart
)
}
// switchMap — cancel previous request on new emission (search autocomplete)
private searchTerm$ = new Subject<string>()
search$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((term) =>
term.length < 2
? EMPTY
: this.http.get<Product[]>('/api/search', { params: { q: term } }).pipe(
catchError(() => of([]))
)
)
)
search(term: string) {
this.searchTerm$.next(term)
}
// shareReplay — cache and share among multiple subscribers
private categories$ = this.http.get<string[]>('/api/categories').pipe(
shareReplay(1) // replay the latest value to new subscribers
)
getCategories(): Observable<string[]> {
return this.categories$
}
// tap — side effects without transformation (logging)
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products').pipe(
tap((products) => console.log('Fetched', products.length, 'products')),
map((products) => products.filter((p) => p.price > 0))
)
}
}HTTP Interceptors for JSON Headers
Interceptors run for every HTTP request and response. They are the correct place to add global headers like Authorization, log requests, or handle 401 errors centrally.
// auth.interceptor.ts — functional interceptor (Angular 15+)
import { HttpInterceptorFn } from '@angular/common/http'
import { inject } from '@angular/core'
import { AuthService } from './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({
setHeaders: {
'Authorization': `Bearer ${token}`,
// HttpClient already sets Content-Type: application/json for post/put/patch
// but you can force it here for all requests:
// 'Content-Type': 'application/json',
},
})
return next(authReq)
}
// logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http'
import { tap } from 'rxjs/operators'
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
console.log(`[${req.method}] ${req.url}`)
return next(req).pipe(
tap({
next: (event) => console.log('Response:', event),
error: (err) => console.error('Error:', err.status, err.url),
})
)
}
// error.interceptor.ts — global 401 redirect
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((err: HttpErrorResponse) => {
if (err.status === 401) {
router.navigate(['/login'])
}
return throwError(() => err)
})
)
}
// Register in main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { provideHttpClient, withInterceptors } from '@angular/common/http'
import { authInterceptor, loggingInterceptor, errorInterceptor } from './interceptors'
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, loggingInterceptor, errorInterceptor])
),
],
})For class-based interceptors (legacy Angular modules), implement HttpInterceptor and provide with HTTP_INTERCEPTORS:
// Class-based interceptor — for NgModule-based apps
import { Injectable } from '@angular/core'
import {
HttpInterceptor, HttpRequest, HttpHandler, HttpEvent
} from '@angular/common/http'
import { Observable } from 'rxjs'
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = localStorage.getItem('token')
if (!token) return next.handle(req)
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
})
return next.handle(authReq)
}
}
// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http'
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
]Angular 17+ Standalone Components
Angular 17+ promotes standalone components as the default. The inject() function replaces constructor injection, and provideHttpClient() replaces HttpClientModule.
// main.ts — standalone bootstrap
import { bootstrapApplication } from '@angular/platform-browser'
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http'
import { AppComponent } from './app/app.component'
import { authInterceptor } from './app/interceptors/auth.interceptor'
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withFetch(), // use browser Fetch API under the hood
withInterceptors([authInterceptor]), // functional interceptors
),
],
})
// product.component.ts — standalone component with inject()
import { Component, inject, signal } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { AsyncPipe, NgFor, NgIf } from '@angular/common'
import { catchError, of } from 'rxjs'
interface Product {
id: number
name: string
price: number
}
@Component({
selector: 'app-products',
standalone: true,
imports: [AsyncPipe, NgFor, NgIf],
template: `
@if (error()) {
<p class="error">{{ error() }}</p>
}
@if (products$ | async; as products) {
<ul>
@for (product of products; track product.id) {
<li>{{ product.name }} — ${{ product.price }}</li>
}
</ul>
}
`,
})
export class ProductsComponent {
// inject() — no constructor needed
private http = inject(HttpClient)
// Signals for local state (Angular 17+)
error = signal<string | null>(null)
products$ = this.http.get<Product[]>('/api/products').pipe(
catchError((err) => {
this.error.set('Failed to load products. Please try again.')
return of([])
})
)
}
// Using toSignal() for signal-based reactive data
import { toSignal } from '@angular/core/rxjs-interop'
@Component({ standalone: true, template: `
@for (p of products(); track p.id) {
<li>{{ p.name }}</li>
}
` })
export class ProductsSignalComponent {
private http = inject(HttpClient)
// Convert Observable to Signal — no subscribe, no async pipe needed
products = toSignal(
this.http.get<Product[]>('/api/products'),
{ initialValue: [] }
)
}Key Terms
- Observable
- A lazy push-based data stream from RxJS.
HttpClientmethods return Observables that emit the HTTP response once. Observables do nothing until subscribed — unlike Promises, an un-subscribedhttp.get()never fires the request. - HttpClient
- Angular's built-in HTTP service. Wraps the browser's
XMLHttpRequest(orfetchwithwithFetch()), adds automatic JSON serialization/deserialization, interceptor support, typed generics, and progress events. - Interceptor
- A middleware function (or class) that runs for every HTTP request and response. Used to add headers, log activity, handle global errors, or transform request/response bodies without modifying individual service methods.
- RxJS operator
- A pure function that takes an Observable and returns a new Observable with modified behavior. Operators like
map,catchError,switchMap, andretryare composed withpipe()to build declarative data pipelines. - async pipe
- An Angular template pipe (
| async) that subscribes to an Observable or Promise, renders the latest emitted value, and automatically unsubscribes when the component is destroyed. The preferred way to consumeHttpClientObservables in templates.
FAQ
How do I fetch JSON from an API in Angular?
Inject HttpClient into a service and call this.http.get<T>(url). Angular parses the JSON response automatically — no response.json() needed. The method returns an Observable<T>. In the component, either subscribe: .subscribe(data => this.data = data), or use the async pipe in the template: data$ | async.
Do I need to call response.json() with Angular HttpClient?
No. HttpClient automatically deserializes JSON responses. The Observable emits a fully parsed JavaScript object. This differs from the browser's fetch API which returns a Response object requiring .json(). Angular also sets Accept: application/json on every request by default.
How do I type HTTP responses in Angular?
Pass a TypeScript interface as the generic parameter: this.http.get<Product>(url) or this.http.get<Product[]>(url). Define an interface matching the JSON shape. For wrapped responses, create a generic wrapper: interface ApiResponse<T> { data: T; total: number } then this.http.get<ApiResponse<Product[]>>(url).
How do I POST JSON data with Angular HttpClient?
Call this.http.post<ResponseType>(url, body). Angular serializes the body object to JSON and sets Content-Type: application/json automatically. Do not call JSON.stringify on the body yourself. The same applies to put() and patch().
What RxJS operators are useful for HTTP JSON requests in Angular?
The most important operators are: map (transform response shape), catchError (handle errors, return fallback), switchMap (cancel previous requests — search autocomplete), retry (automatic retry on failure), tap (logging without transforming), and shareReplay(1) (cache and multicast). Compose them in pipe().
How do I handle HTTP errors when fetching JSON in Angular?
Use catchError in the pipe() chain. It receives an HttpErrorResponse with status, statusText, and error (the parsed JSON error body). Return a safe fallback with of([]) or EMPTY, or rethrow with throwError(() => err). For global error handling across all requests, use an HttpInterceptor.
What is an HttpInterceptor and when should I use one?
An HttpInterceptor intercepts every HTTP request and response. Use interceptors to add Authorization headers globally, log all requests, handle 401 redirects, or retry on failure — without modifying individual services. In Angular 17+ standalone apps, use functional interceptors with provideHttpClient(withInterceptors([myInterceptor])).
How does inject(HttpClient) work in Angular 17+ standalone components?
inject(HttpClient) replaces constructor injection in Angular 17+ standalone components. Call it in the class body (not inside a method): private http = inject(HttpClient). It must run in an injection context — during class instantiation. Register HttpClient via provideHttpClient() in bootstrapApplication()'s providers array.
Further reading and primary sources
- Angular HttpClient Guide — Official Angular documentation for HTTP client
- RxJS Operators — Complete RxJS operator reference
- Angular Standalone Components — Angular 17+ component model documentation