Tutorials Logic, IN info@tutorialslogic.com

Angular HttpClient GET, POST, PUT, DELETE

HttpClient Contract

HttpClient provides typed request APIs, interceptors, progress events, and RxJS cancellation, but its generic types do not validate server data at runtime. Configure the client deliberately and keep transport DTOs at the application boundary.

HttpClient is Angular's typed Observable API for HTTP requests. In current Angular applications it is injectable by default; provideHttpClient remains the configuration point for features such as functional interceptors and alternative backend options.

The default backend uses fetch in current Angular, while withRequestsMadeViaParent and withInterceptors configure composition at injector boundaries and withXhr selects XMLHttpRequest when an application specifically requires it.

A request observable is cold: no request is sent until subscription, and separate subscriptions can send separate requests. Return observables from services and let the owning component, signal conversion, or template choose the subscription lifetime.

The generic type describes the response shape to TypeScript; it does not validate untrusted JSON at runtime. Validate critical API data before treating it as a domain object.

  • Generic response types are compile-time assertions, not runtime decoders.
  • HttpParams, HttpHeaders, HttpRequest, and HttpResponse are immutable.
  • Unsubscribing can abort an in-flight request; operator choice determines cancellation ownership.
  • Interceptors apply cross-cutting transport policy and must preserve request security boundaries.
  • HttpTestingController verifies requests without a live server.

HttpClient Setup

HttpClient is available for injection by default in Angular v21 and later. Add provideHttpClient to application providers when configuring features such as functional interceptors, XSRF options, JSONP, a parent injector chain, or the XHR backend.

  • Inject HttpClient into a service or component with inject(HttpClient) or constructor injection.
  • Return the request observable from a service so the owning view can choose subscription, cancellation, and display behavior.
  • Use the async pipe for template streams or toSignal when the state is more naturally consumed as a signal.

app.config.ts

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

Inject HttpClient

Inject HttpClient
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class DataService {
  private http = inject(HttpClient);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users');
  }
}

Use in Component

Use in Component
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { DataService } from './data.service';

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    @for (user of users(); track user.id) {
      <p>{{ user.name }} - {{ user.email }}</p>
    }
  `
})
export class UsersComponent {
  private dataService = inject(DataService);
  users = toSignal(this.dataService.getUsers(), { initialValue: [] });
}

HTTP Request Methods

All HTTP methods return an Observable. You must subscribe or use toSignal() to get the data.

Use HttpParams and HttpHeaders because request objects are immutable; assignment methods return new instances. Set observe to response when status or headers matter and responseType when the payload is text, blob, or array buffer.

GET should be safe and cacheable according to the API contract. Retry idempotent operations cautiously; do not retry a payment or other write unless the server supports an idempotency key or equivalent contract.

Unsubscribing cancels an in-flight request when supported. switchMap is useful for typeahead or route-driven requests because it unsubscribes the obsolete inner request when a newer value arrives.

HTTP Methods

HTTP Methods
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

const BASE = 'https://jsonplaceholder.typicode.com';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);

  // GET
  getPost(id: number) {
    return this.http.get(`${BASE}/posts/${id}`);
  }

  // POST
  createPost(data: object) {
    return this.http.post(`${BASE}/posts`, data);
  }

  // PUT
  updatePost(id: number, data: object) {
    return this.http.put(`${BASE}/posts/${id}`, data);
  }

  // PATCH
  patchPost(id: number, data: object) {
    return this.http.patch(`${BASE}/posts/${id}`, data);
  }

  // DELETE
  deletePost(id: number) {
    return this.http.delete(`${BASE}/posts/${id}`, { responseType: 'text' });
  }
}

Request Options and Response Shapes

Use params for URL query data and headers for request metadata. Because both classes are immutable, keep the value returned by set, append, or delete. Pass observe: "response" when status and response headers are part of the feature contract.

Set responseType to text, blob, or arraybuffer for non-JSON payloads. When options are extracted into a variable, preserve literal types with as const so TypeScript selects the correct HttpClient overload.

Progress events require observe: "events" and reportProgress: true. The Fetch backend does not report upload progress, so an upload UI that depends on it needs the XHR backend and explicit browser testing.

  • Use HttpParams.fromObject for a known query object and a custom encoder only when the API contract requires it.
  • Read pagination or correlation headers from HttpResponse rather than assuming they are in the JSON body.
  • Do not set Content-Type manually for FormData; the browser supplies the multipart boundary.

Typed Search with Full Response Metadata

Typed Search with Full Response Metadata
search(term: string, page: number) {
  const params = new HttpParams()
    .set('q', term)
    .set('page', page);

  return this.http.get<readonly ProductDto[]>('/api/products', {
    params,
    observe: 'response' as const
  }).pipe(
    map(response => ({
      products: response.body ?? [],
      total: Number(response.headers.get('X-Total-Count') ?? 0)
    }))
  );
}

Upload Progress Events

Upload Progress Events
upload(file: File) {
  const body = new FormData();
  body.append('file', file);

  return this.http.post('/api/uploads', body, {
    observe: 'events',
    reportProgress: true
  }).pipe(
    map(event => event.type === HttpEventType.UploadProgress
      ? Math.round(100 * event.loaded / (event.total ?? event.loaded))
      : null)
  );
}

Failures Timeout and Cancellation

HttpClient reports backend status failures and network failures as HttpErrorResponse. A status of 0 commonly indicates a network, CORS, or client-side failure; a nonzero status came from an HTTP response. Preserve that distinction when mapping transport errors to application states.

Use the request timeout option for a transport deadline and RxJS timeout when a broader stream contract needs it. Retrying is safe only when the operation is idempotent or the server honors an idempotency key.

Unsubscribing cancels the active request. switchMap gives newer route parameters or search terms ownership by unsubscribing the previous request; takeUntilDestroyed binds a manual subscription to its Angular owner.

  • Do not expose raw server messages directly to users; map known errors and log redacted diagnostic context.
  • Keep a failed refresh separate from initial-load failure when stale data can remain useful.
  • Place catchError inside switchMap when one failed search must not terminate future searches.

Cancel Stale Search Requests

Cancel Stale Search Requests
readonly results$ = this.searchControl.valueChanges.pipe(
  map(value => value.trim()),
  debounceTime(250),
  distinctUntilChanged(),
  switchMap(term => this.http.get<readonly Product[]>('/api/products', {
    params: { q: term },
    timeout: 5000
  }).pipe(
    catchError(error => of({ error, products: [] as readonly Product[] }))
  ))
);

Testing HTTP Contracts

Register provideHttpClient before provideHttpClientTesting, call the service method, then capture the request with HttpTestingController. Assert method, URL, parameters, headers, and body before flushing a controlled response.

Use flush with a status and statusText for backend failures and ProgressEvent for a network-level error. Call verify after each test so an unexpected duplicate or unflushed request fails the suite.

  • Subscribe before expectOne because the request Observable is cold.
  • Use match when the behavior intentionally sends several requests.
  • Test runtime response validation separately if the service decodes untrusted JSON.

Verify a GET Request

Verify a GET Request
TestBed.configureTestingModule({
  providers: [
    ProductService,
    provideHttpClient(),
    provideHttpClientTesting()
  ]
});

const service = TestBed.inject(ProductService);
const http = TestBed.inject(HttpTestingController);

service.getProduct(7).subscribe(product =>
  expect(product.name).toBe('Keyboard')
);

const request = http.expectOne('/api/products/7');
expect(request.request.method).toBe('GET');
request.flush({ id: 7, name: 'Keyboard' });
http.verify();

Interceptor Configuration Boundary

Prefer functional interceptors and register them with withInterceptors in provideHttpClient. Class-based interceptors remain available through the compatibility configuration, but functional ordering is more predictable.

Functional Interceptor

Functional Interceptor
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('token');
  if (token) {
    const authReq = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    });
    return next(authReq);
  }
  return next(req);
};

Register Functional HTTP Interceptors

Register Functional HTTP Interceptors
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};
Confirm the page outcome

HTTP Review

5 checks
  • I can configure HttpClient features and explain when the Fetch or XHR backend matters.
  • I know that every subscription to a cold request can send another network call.
  • I can choose typed bodies, params, headers, observe, responseType, progress, timeout, and cancellation deliberately.
  • I distinguish transport failures, backend status errors, empty success responses, and runtime validation failures.
  • I can test the request contract with HttpTestingController and verify that no unexpected requests remain.

HTTP Review Questions

A subscription. HttpClient returns a cold Observable, so calling the method only builds the request.

The generic type is a compile-time assertion for TypeScript; HttpClient does not validate the server response at runtime. If the API returns { data: [...] } or malformed user objects, the code can compile and still fail when the template reads user.id or user.email.

The Fetch backend is useful for modern browser behavior and server rendering, but it is not identical to XMLHttpRequest. Applications that depend on upload progress events, certain legacy interceptor assumptions, or browser behaviors specific to XHR need testing before switching.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.