The 5 Minute Guide to Making HTTP Requests in Angular

Importing the HttpClientModule

Angular uses the HttpClientModule to make async HTTP requests from the browser. To use the HttpClientModule, you must first import it in your app.module.ts file:

import { HttpClientModule } from '@angular/common/http';

This imports the HttpClientModule so you can use it in your project.

Also be sure to include the HttpClientModule after the BrowserModule in the imports section of the app.module.ts file.

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ]

Notice how we include the HttpClientModule in the list of imports in our @NgModule definition. It's important that we import this AFTER the BrowserModule.

Creating a service

It's considered best practice to make all of your HTTP requests from a service. This clearly separates data retrieval logic from presentation logic and allows you to reuse code across components.

You typically create a shared service and then reference it from your components. Let's say you want to make a GET request to some API that returns a list of users. You'd first create a UserService:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'

@Injectable()
export class UserService {
  userUrl = '/api/users'
  constructor(private http: HttpClient) { }

  getUsers(){
    return this.http.get(this.userUrl)
  }
}

Notice how we've created a UserService that imports HttpClient. We define a getUsers() method which returns an HTTP GET call to /api/users. For more information on creating services, check out this quick example.

Now we can call this getUsers() method from a component:

import { Component } from '@angular/core';
import { UserService } from './user.service'
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  private users = [];
  constructor(private userService:UserService){}

  listUsers() {
    this.userService.getUsers()
      .subscribe(
        data => {
          this.users = data['users']
        }
      )
  }

  ngOnInit(){
    this.listUsers()
  }
}

Notice how we've imported the UserService so we can reference it in our AppComponent. We make the actual HTTP request within our listUsers() method by calling this.userService.getUsers().subscribe()

We have to call subscribe() because our UserService returns an Observable. Observables are basically blue prints for HTTP requests. When you subscribe() to the returned observable, you execute the actual HTTP request.

While Observables are outside the scope of this 5 minute example, you can click here to understand how Observables work in more detail.

Also notice how we map the returned JSON data to our this.users variable. The object we pass into subscribe() is called an observer. An observer object defines callback methods based on events thrown by the Observable.

While our example defines a single handler data => {}, we could optionally provide handlers for errors and completion of the Observable:

.subscribe(
  data => {
    this.users = data['users']
  },
  error => {
    console.log(error)
  },
  () => {
    console.log("Request complete")
  }
)

Our first callback data = {} is the only required argument for the observer. We can optionally provide an error => {} callback for error handling (however it's better to handle this in the service). The last callback is run on completion and is comparable to finally() in a try/catch block. This runs regardless of whether or not an error is thrown.

POST Requests

Sending data via POST requests is very similar to GET requests. In our same UserService class, we can define a POST request for creating a new user:

createUser(userObject){
    let httpOptions = {}
    return this.http.post(this.userUrl, userObject, httpOptions)
}

Then we can interact with the service from our component:

postUser() {
  let user = {first:"Sam", last:"Smith", email:"sam@gmail.com"}
  this.userService.createUser(user)
    .subscribe(
      data => {
        console.log("user created!")
      }
    )
}

Notice how we define a user object from within the postUser() method of our component. We then pass this into the this.userService.createUser() method.

The only major difference is the data we pass with the request. The createUser() method takes a single userObject parameter and passes this into this.http.post().

Notice how this.http.post() takes three arguments. The first is the URL and the second is the data to post. The third argument is optional and allows you to pass in custom HTTP headers, etc.

For this example, we've simply passed an empty object for the optional httpOptions parameter.

PUT Requests

A PUT requests works the same as a POST request:

updateUser(userObject){
    let httpOptions = {}
    return this.http.put(this.userUrl, userObject, httpOptions)
}

Notice how upateUser() is nearly identical to createUser() apart from specifying this.http.put vs this.http.post.

DELETE Requests

A DELETE request works the same as a GET request:

deleteUser(id){
  const url = '${this.userUrl}/${id}'
  let httpOptions = {}
  return this.http.delete(url, httpOptions)
}

Notice how we dynamically set the url based on an id passed to deleteUser(). We can also optionally specify httpOptions.

That's it! While the HttpClientModule supports advanced features for things like intercepting requests, setting XSRF tokens, and modifying request headers, this covers the basics of making HTTP requests with Angular. Keep reading to explore some more advanced concepts surrounding async HTTP requests in Angular.

The HTTPClientModule: A Deeper Dive

The Angular HttpClient rests on top of the XMLHttpRequest interface exposed by most modern browsers. It leverages the RxJS library and the observable design pattern to more gracefully handle async data streams.

Using RxJS operators, you can easily manipulate and transform these data streams.

Handling Errors

HTTP errors can happen in several ways. The server may return a 404 or 500 if it's a bad request. The client may fail to make a request because of a network error.

Regardless of how these errors happen, it's important that your code catches these errors and handles them appropriately.

Remember that the observer object is what we pass to the subscribe() method in our component. You already saw how we can add an optional error => callback in our observer object. While we can define this callback in the observer, it's considered best practice to handle errors from the service rather than the component:

private handleError(){
  return throwError(
    'An error has occurred'
  )
}
getUsers(){
  return this.http.get(this.userUrl)
    .pipe(
      catchError(this.throwError)
    )
}

From the component's perspective, this still works the same way. The component subscribes to the getUsers() method and handles the response.

The key difference is the pipe() operator. The pipe operator combines different functions you want to perform on the data stream as values are returned. In this particular case, we use the RxJS catchError operator to return a new Observable with an error message. We import the RxJS throwError method to create a new Observable in our handleError() method.

By returning a new Observable with an error message, our component can subscribe to the getUsers() in the same way.

Please note: For this code to run, you must import both catchError and throwError from RxJS:

import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';

Type Checking Responses

A key advantage of the Angular HttpClientModule is the ability to type check responses. Let's revisit the listUsers() method in our sample component:

listUsers() {
  this.userService.getUsers()
    .subscribe(
      data => {
        this.users = data['users']
      }
    )
}

Typescript complains if we don't use the brackets in data['users'], and for good reason.

By creating an interface for your data model, you can more concisely consume output from HTTP requests:

export interface User {
  first: string;
  last: string;
  email: string;
}

Once you create an interface, you can import it from your components and services and specify the data types being returned:

in the component...

listUsers() {
  this.userService.getUsers()
    .subscribe(
      (data: User) => {
        this.user = data
      }
    )
}

in the service...

getUsers(){
  return this.http.get<User>(this.userUrl)
    .pipe(
      catchError(this.throwError)
    )
}

HTTP Options

You can optionally supply an httpOptions argument to manipulate HTTP headers and response types:

getUsers(){
  return this.http.get<User>(this.userUrl, {observe : 'response'})
    .pipe(
      catchError(this.throwError)
    )
}

By default, the HttpClient returns only part of the full response. By specifying {observe: 'response'}, the client returns a full response object, including the response headers and body.

getUsers(){
  return this.http.get<User>(this.userUrl, {responseType: 'text'})
    .pipe(
      catchError(this.throwError)
    )
}

By default, the HttpClient returns JSON formatted responses. By specifying {responseType: 'text'}, the client returns a text response.

For more examples of manipulating response types and headers, be sure to check out the official documentation. You will find examples of using interceptors to modify incoming / outgoing requests as well as advanced concepts on configuring XSRF.

Conclusion

The HttpClient leverages the Observable design pattern to handle async data streams gracefully. Use services to return observables that you subscribe to from components. Perform error handling in your services and create interfaces for your data objects to more cleanly consume responses from the HttpClient.

Your thoughts?