New to Rust? Grab our free Rust for Beginners eBook Get it free →
Making HTTP Calls in Angular with HttpClient

Angular reports a provider error when an old tutorial imports Http from @angular/http because that package no longer belongs in a new application. I generated a fresh Angular 22.1.0 project and tested its GET, POST, and failure paths so you can make HTTP calls in Angular with the HttpClient API that ships today.
How Angular sends HTTP requests today
Angular exposes HttpClient from @angular/common/http. As of Angular 21, the service is available for dependency injection by default, while provideHttpClient remains the configuration entry point for features such as interceptors and an alternate backend.
Each request method returns a Reactive Extensions for JavaScript (RxJS) Observable that waits for a subscription, and a second subscription sends a second request.
| Old Angular code | Current Angular code |
|---|---|
| Http from @angular/http | HttpClient from @angular/common/http |
| Response.json() | Automatic JSON parsing |
| Prototype map import | pipe() with RxJS operators |
| HttpModule | HttpClient available by default in Angular 21 and later |
An NgModule application can keep HttpClientModule while you migrate, but new standalone projects do not need it for basic requests.
Create a fresh Angular project
The following command created the project used for every sample on this page. It installs the newest Angular CLI release available through npm instead of selecting an older framework version.
npx @angular/cli new angular-http-demo --defaults --skip-git --style=css --ssr=false --package-manager=npm --no-interactive
The execution used Node.js 24.18.0, npm 11.16.0, Angular 22.1.0, Angular CLI 22.1.2, RxJS 7.8.2, and TypeScript 6.0.3. Your exact patch releases may differ when npm publishes an update.
Put HTTP calls in an injectable service
A service keeps endpoint details and response handling away from the component while giving HttpTestingController one focused dependency to inspect.
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { catchError, Observable, throwError } from 'rxjs';
export interface Post {
id: number;
userId: number;
title: string;
body: string;
}
export type NewPost = Omit<Post, 'id'>;
@Injectable({ providedIn: 'root' })
export class PostService {
private readonly http = inject(HttpClient);
private readonly apiUrl = 'https://jsonplaceholder.typicode.com/posts';
getPost(id: number): Observable<Post> {
return this.http.get<Post>(`${this.apiUrl}/${id}`).pipe(
catchError(() => throwError(() => new Error('Could not load the post.')))
);
}
createPost(post: NewPost): Observable<Post> {
return this.http.post<Post>(this.apiUrl, post);
}
}
The Post interface describes the expected response, but the generic type on get() does not validate data at runtime, so check untrusted or changing JSON before your component relies on those fields.
Send a GET request
getPost() builds a URL with the requested identifier and returns Observable<Post>. Nothing is sent while the method only returns that Observable.
A subscription starts the GET request, then HttpClient parses a JSON response and emits the body. The stream normally completes after one response, although an interceptor can alter that behavior.
Keep the subscription close to the consumer. A template can use Angular’s async pipe, while imperative code can subscribe with next and error handlers when it must update local state or trigger another action.
Send a POST request
createPost() passes the target URL and a plain object to HttpClient.post(), which serializes that body as JSON without manual stringify() or a Content-Type header.
The returned Observable still needs a subscription. This is easy to miss with mutation requests because calling createPost() by itself looks like an action but only constructs the request stream.
If your server expects form data, plain text, or URL-encoded values, pass that body type instead of forcing JSON. Angular selects serialization from the body value, as documented in the official request guide.
Handle failures without hiding the cause
HttpClient sends network failures, timeouts, and backend errors through the Observable error channel. Backend errors retain the returned status code, while network and timeout failures use status 0.
The service converts either case into a short message for its caller. In a production application, record the original HttpErrorResponse in an approved logging path before replacing it, but do not expose server internals or credentials in the interface.
A transient GET can sometimes be retried, but automatically repeating POST requests may create duplicate records unless the endpoint implements idempotency.
Test requests without calling an external API
The @angular/common/http/testing package replaces the network backend with HttpTestingController so your test can subscribe, inspect the request, and flush a controlled response.
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';
import { PostService } from './post.service';
describe('PostService', () => {
let service: PostService;
let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [PostService, provideHttpClientTesting()],
});
service = TestBed.inject(PostService);
httpTesting = TestBed.inject(HttpTestingController);
});
afterEach(() => httpTesting.verify());
it('sends a GET request and returns a typed post', async () => {
const resultPromise = firstValueFrom(service.getPost(1));
const request = httpTesting.expectOne(
'https://jsonplaceholder.typicode.com/posts/1'
);
expect(request.request.method).toBe('GET');
request.flush({ id: 1, userId: 1, title: 'Hello', body: 'From the API' });
await expect(resultPromise).resolves.toEqual({
id: 1,
userId: 1,
title: 'Hello',
body: 'From the API',
});
});
it('sends a POST request with a JSON body', async () => {
const newPost = { userId: 1, title: 'New post', body: 'Saved' };
const resultPromise = firstValueFrom(service.createPost(newPost));
const request = httpTesting.expectOne(
'https://jsonplaceholder.typicode.com/posts'
);
expect(request.request.method).toBe('POST');
expect(request.request.body).toEqual(newPost);
request.flush({ id: 101, ...newPost });
await expect(resultPromise).resolves.toEqual({ id: 101, ...newPost });
});
it('converts a backend failure into a user-safe error', async () => {
const resultPromise = firstValueFrom(service.getPost(404));
const request = httpTesting.expectOne(
'https://jsonplaceholder.typicode.com/posts/404'
);
request.flush('Missing', { status: 404, statusText: 'Not Found' });
await expect(resultPromise).rejects.toThrow('Could not load the post.');
});
});
In my execution receipt, the GET assertion, POST body assertion, and 404 conversion all pass against the generated Angular project, which also completes a production build.

When you add provideHttpClient() for interceptors or another feature, place it before provideHttpClientTesting() because the testing provider replaces parts of the normal backend.
Headers, parameters, and full responses
Most applications eventually need query parameters, request headers, or the status code from the whole response. HttpClient accepts those controls through its options object.
| Need | HttpClient option | Important boundary |
|---|---|---|
| Query string | params | HttpParams is immutable, so set() returns a new instance |
| Request headers | headers | HttpHeaders is also immutable |
| Status and response headers | observe set to ‘response’ | The return type changes from the body to HttpResponse |
| Text or binary data | responseType | Use a literal value so TypeScript infers the correct result |
| Request deadline | timeout | The limit applies to the backend request, not delays inside interceptors |
A functional interceptor can add authentication credentials in one place, but it should restrict them to trusted origins and must never send a token to an arbitrary URL.
CORS and browser security boundaries
A correct Angular request can still fail because Cross-Origin Resource Sharing (CORS) is enforced by the browser, and your Angular code cannot grant itself permission to read a blocked response.
Configure the API to allow the application origin, methods, and required headers. If your backend is PHP, the Angular POST request to PHP example shows the server side of the exchange, while an Angular application on Firebase gives you another deployment path to compare.
Keep API keys off browser bundles because any value shipped to the frontend can be inspected, which puts secret-bearing calls behind a backend you control.
Common Angular HTTP failures
- NullInjectorError for HttpClient. Check that the project uses a supported Angular release and that an older custom bootstrap has not removed the provider.
- The request never appears. Subscribe to the Observable or consume it through a template helper such as the async pipe.
- The same endpoint runs twice. Look for two subscriptions to the same cold Observable.
- The response type looks correct but breaks at runtime. The generic is a TypeScript assertion, not JSON validation.
- The browser reports a CORS failure. Change the API response headers rather than trying to bypass the browser in Angular.
Use the test as your final check
Start with one service method and one HttpTestingController expectation. If the test proves the verb, URL, body, success value, and failure value, the component can stay focused on display and interaction.
Run the suite with the command shown in the screenshot, then inspect the browser Network panel against your own API. A passing mock test proves the Angular request contract, while the browser check proves deployment details such as CORS, authentication, and the server response.
Frequently asked questions
Does Angular HttpClient need HttpClientModule?
Angular 21 and later make HttpClient available for injection by default. Older NgModule applications can continue to import HttpClientModule while they migrate.
Why does an Angular HTTP request not run?
HttpClient returns a cold Observable. Subscribe to it, use the async pipe, or convert it with an RxJS helper before expecting the request to start.
Does HttpClient validate a typed JSON response?
No. The generic type tells TypeScript what you expect, but HttpClient does not check the returned JSON against that interface at runtime.
Can Angular fix a CORS error?
No. The API must allow the application origin and required request details in its CORS response headers.




