Introduction
With all the wonderful treats that the Angular team has given us during the recent "renaissance" era, many new developers are joining in on the fun. And one of the challenges they'll face at some point is how to call an API from your Angular application properly. Unfortunately, while searching for a guide on how to do this, they might stumble upon a lot of outdated information. Hence, this article should serve as a reliable guide on how to use the HttpClient
in Angular >= 17.
The Setup
To make an HTTP request in Angular, you can take advantage of the HttpClient
provided by the @angular/common/http
package. To use it, you'll need to provide it. Here's how you can do that for the whole application using the bootstrapApplication
function from @angular/platform-browser
:
import { provideHttpClient } from "@angular/common/http";
import { bootstrapApplication } from "@angular/platform-browser";
bootstrapApplication(App, {
providers: [provideHttpClient()],
});
With that, you should be good to go. You can now inject the HttpClient
into any service or component in your application.
Using HttpClient in Services
Let's take a common example: You have a database object, say a Movie,
and you want to implement CRUD operations on it. Typically, you'll want to create a service that provides methods for these operations. Let's call this service MovieService
and create a skeleton for it with a method for getting all movies.
// movie.service.ts
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class MovieService {
// Inject the HttpClient in constructor
constructor(private httpClient: HttpClient) {}
getAllMovies() {
// TODO: Implement this method
}
}
Implementing the Method using HttpClient
Let's assume we have a GraphQL API for our movies. We can implement our getAllMovies
using HttpClient
to make a request to fetch all movies.
First, we will need to define a new type to represent the response from the API. This is especially important when you are using GraphQL, which may return a specific structure, such as:
{
"data": {
"allMovies": {
"edges": [
{
"id": 1,
"title": "One Flew Over the Cuckoo's Nest",
"director": "Milos Forman",
"releaseDate": "1975-11-19"
},
{
"id": 2,
"title": "The Shining",
"director": "Stanley Kubrick",
"releaseDate": "1980-05-23"
}
]
}
}
}
When working with a real API, you'll likely use some code generator to generate the types for the response from the GraphQL schema. But for the sake of this example, we'll create an interface to represent the response manually:
// movie-api.model.ts
import { Movie } from "./movie.model";
export interface MoviesListResponse {
data: {
allMovies: {
edges: Movie[];
};
};
}
Now, we can implement the getAllMovies
method using HttpClient
:
getAllMovies(): Observable<MoviesListResponse> {
// The graphql query to fetch all movies
const query = `{
allMovies(first: 10) {
edges {
id
title
director
releaseDate
}
}
}`;
return this.httpClient.post<MoviesListResponse>(
'https://api.example.com/graphql',
{
query,
},
{
headers: {
'Content-Type': 'application/json',
},
}
);
}
Note: The
post
method is used here because we are sending a request body. If you are making aGET
request (e.g. to a REST API), you can use theget
method instead.
The getAllMovies
method returns an Observable
of MoviesListResponse
. In this example, I have typed it explicitly to make it obvious at first glance, but you could also omit the type annotation, and TypeScript should infer it.
Note: While I'm excited about signals as much as the next guy, making HTTP requests is one of the typical async operations for which RxJS Observables are a perfect fit, making a great argument for RxJS still having a solid place in Angular alongside signals.
Using the Service in a Component
Now that we have our MovieService
set up, we can use it as a component to fetch and display all movies.
But first, let's create a Movie
interface to represent the structure of a movie. Trust me, this will prevent many potential headaches down the line. Although using any
at the beginning and implementing the types later is a valid approach in some cases, using data fetched from an API without validating the type will inevitably lead to bugs that are difficult to solve.
// movie.model.ts
export interface Movie {
id: number;
title: string;
director: string;
releaseDate: Date;
}
Now, we can start implementing our standalone MoviesComponent
:
// movies.component.ts
import { Component, inject } from "@angular/core";
import { MovieService } from "./movie.service";
import { Movie } from "./movie.model";
@Component({
selector: "app-movies",
standalone: true,
imports: [],
templateUrl: "./movies.component.html",
})
export class MoviesListComponent {
// We are using the "inject" function to inject the MovieService but we could also use the constructor
movieService = inject(MovieService);
// The object to hold the movies using the Movie interface
movies: Movie[] = [];
constructor() {
// Call the method to fetch all movies
this.movieService.getAllMovies().subscribe((response) => {
// Assign the movies to the movies property
this.movies = response.data.allMovies.edges;
});
}
}
In this component, we are calling the getAllMovies
method from the MovieService
in the constructor to fetch all movies and assign them to the movies
property which we will use to display the movies in the template.
<!-- movies.component.html -->
<ul>
@for(movie of movies; track movie.title) {
<li>{{ movie.title }}</li>
}
</ul>
In this case, placing our code inside the constructor is safe because it doesnβt depend on any @Input()
. If it were, our code would fail because the inputs arenβt initialized at time of instantiation. That's why it is sometimes recommended to place logic in ngOnInit
instead. You could also put this call in an arbitrary method that is called e.g. on a button click.
Another way to handle the subscription is to use the async
pipe in the template. This way, Angular will automatically subscribe and unsubscribe from the observable for you and you won't have to assign the response to a property in the component. Due to the structure of the returned data, however, we'll need to use the RxJS map
operator to extract the movies from the response.
// movies.component.ts
import { AsyncPipe } from '@angular/common';
import { Component, inject } from "@angular/core";
import { map, Observable } from 'rxjs';
import { Movie } from './movie.model';
import { MovieService } from "./movie.service";
@Component({
selector: "app-movies",
standalone: true,
imports: [AsyncPipe], // We need to import the AsyncPipe to have it available in the template
templateUrl: "./movies.component.html",
})
export class MoviesListComponent {
movieService = inject(MovieService);
// The observable to hold the movies
movies$: Observable<Movie[]> = this.movieService.getAllMovies().pipe(
map(({data}) => data.allMovies.edges)
);
}
Note using the
pipe
method to chain themap
operator to the observable returned bygetAllMovies
. This is a common pattern when working with RxJS observables introduced in RxJs 5.5. If you see code that uses themap
operator directly on the observable and wonder why it isn't working for you, it's likely using an older version of RxJS.
Now, we can simply apply the async
pipe in the template to subscribe to the observable and display the movies:
<!-- movies.component.html -->
<ul>
@for(movie of movies$ | async; track movie.title) {
<li>{{ movie.title }}</li>
}
</ul>
Conclusion
HttpClient in Angular is pretty straightforward, but with all the changes to RxJS and Angular in the past few years, it can pose a significant challenge if you're not super-experienced with those technologies and stumble upon outdated resources. Following this article, you should hopefully be able to implement your service using HttpClient
to make requests to an API in your modern Angular application and avoid the struggle of copying outdated code snippets.