What do you want to learn?
Skip to main content
Angular HTTP Communication
by Brice Wilson
Nearly every Angular app needs to communicate with a server over HTTP. This course will teach you simple, as well as advanced, techniques to help you create and manage HTTP requests, responses as Observables, interceptors, and client-side caches.
Resume CourseBookmarkAdd to Channel
Table of contents
Hey everybody. My name is Brice Wilson, and welcome to my course, Angular HTTP Communication. I'm a server and client-side web developer. Angular is a wildly-popular client-side web framework and for lots of good reasons. It helps you quickly build well-structured, maintainable, and performance-oriented web applications. In this course, we're going to cover both basic and advanced HTTP features available in Angular that help you successfully communicate with your server, and efficiently retrieve and manage data in your applications. Some of the major topics that we'll cover include consuming REST services, retrieving data with resolvers, creating interceptors, caching data in your apps, and unit testing your HTTP requests. By the end of the course, you'll know how to use all the major features in Angular's HTTP client to retrieve and update data, but also manage that data on the client efficiently, so that your app provides a great experience for your users. Before beginning this course, you should be familiar with the basics of Angular and TypeScript, but you certainly don't need to be an expert. I hope you'll join me on this journey to learn about Angular's HTTP client with the Angular HTTP Communication course at Pluralsight.
Configuring an Application to Make HTTP Requests
Introduction and Overview
Angular is a framework for building web applications, and there is nothing more fundamental to a web application than the HTTP Protocol. Hi, I'm Brice. In this course, we're going to go beyond the sort of automatic use of HTTP most of us take advantage of as web developers. We know our client-side code, images, and other assets get delivered to the browser over HTTP, and you likely know if you're watching this that you can fire off a simple HTTP request in your Angular code to get some data from the server. That's great but most client-side web apps need lots of data from the server. I'm going to show you how to use the APIs built in to Angular to fully harness the power of the HTTP protocol, and precisely control how you retrieve and manage all of the data your app needs. Now, I want to be careful to point out exactly which APIs I'll be using in this course. When Angular 2 was first released in September 2016, you made HTTP calls with it by first importing the HTTP module, and then using a class simply named HTTP that included methods for making HTTP requests. It worked fine, but the API was a little clunky, and it was missing some nice features that developers appreciated in the 1.X version of Angular now known as Angular JS. So, in version 4.3 of Angular, which was released of July 2017, the Angular team added a completely new set of APIs for making HTTP requests. They're in a module named HttpClientModule, and the class you use to make requests is named HttpClient. Beginning with the release of Angular 5 in November 2017, the older HTTP module has been deprecated. So in this course, I will exclusively focus on features available in the newer APIs originally released in version 4.3. Let's now take a closer look at what those features are, and how they fit into the remaining modules of the course. After this introductory module, I'll show you how to consume a REST service using the HttpClient. I'll use the HTTP get, post, put, and delete verbs to retrieve and make changes to data on the server. After that, I'll show you some more advanced techniques, including how to encapsulate HTTP details in a service, make requests with a resolver, and properly handle errors. We'll then see how to create interceptors. They allow you to write a bit of code once that can read and modify all HTTP requests and responses. Once we've implemented some basic interceptors, I'll show you how they can be used to build a client-side cache in your app, saving you from making unnecessary calls to the server. In the last module, I'll show you the built-in HttpClient testing module, and HttpTestingController that help you write unit tests for your HTTP requests. Let's now go have a look at the demo project I'll be using in the course.
Demo: Book Tracker Project Overview
In this demo, I'll give you a quick overview of the app I'll be working on in the course, and how all of the code in the project is structured. I've named the app Book Tracker, and it may look familiar if you watched a couple of my other courses. It's a very simple app that lets kids keep track of the books they have, and how much time they spend reading. This is the main screen in the app, and it's being loaded by an Angular component I named dashboard. As you can see, it has three main sections. The first lists all of the books in the kid's library, the second lists all of the readers using the app, and the third is a short section that lists the current most popular book. All of the data you see on the screen now is currently hardcoded in the client code. Throughout the course, we're going to change that, and I'll show you simple, as well as sophisticated techniques for retrieving it from a server. There's a link at the top of the page for adding a new book. It takes you to a screen where you can enter information about a new book and save your changes. It doesn't work yet, but in the next course module, we'll wire this up to add new books to the server. There's another link at the top for adding new readers. The app title on the left side of the title bar is also a link and takes you back to the dashboard. In addition to the components for adding new books and readers, I've also already got separate components for editing both types of data. Notice that each book has an Edit and a Delete link that will also wire up to make the appropriate HTTP request to the server. Clicking on an Edit link takes you to an edit component that lets you change any of the data about a book, and click the Save button. This screen also has a Set As Most Popular button if you wanted to make this particular book the current most popular book in the library. You can see more about how I use a service to manage the most popular book in my earlier Pluralsight course titled Angular Services. The Delete links don't take you to a separate screen, but we will update them in the next module to actually send requests to the server and perform delete operations on the books. Data about the readers may also be updated and deleted much like books. Okay, that's about it for the functionality right now. Let's now jump over to my code editor, and see how the project is structured. I'll be using Visual Studio Code as my editor. It works great with TypeScript and Angular, but so do a number of other editors, so use whatever you like. I'm not going to go over every file in my project, but I will say that most of what I have here was created with the Angular command line interface, the CLI, therefore it should look reasonably similar to other Angular projects you've seen. There are a handful of additional things here though. The .vscode folder contains a file with a few settings for my editor. The server folder contains all of the code for a simple Node.js server application. It's a pretty simple application I built with the Express framework that will both serve the compiled Angular app to the browser, and provide several API endpoints we'll use in the course to send and receive data to and from the server using HTTP. The data we'll read and write on the server is stored in a couple of text files inside the server folder. There's no fancy database, so I hope that makes it easier for you to get started working with the project yourself. I'll expand the src folder and then the app folder inside it. It contains all of the client-side code. I've got separate folders for each of the Angular components in the app. Each of those folders contains a TypeScript file for the component and an HTML file for the component's view. I've also got a folder named core where I've stored the files for a shared core module. This module provides some of the Angular services in the app, including the data.service here that I'll be using quite extensively in the course. All of the HTTP calls I make to the server will be made from the data.service. A little further down is the models folder. This contains classes for the entities I'll be working with in the app. I'll quickly open the books.ts file, and you can see that it's just a simple TypeScript class containing the properties I'm storing about each book. The services folder is just a place to store any other Angular services I may decide I need in the app. It currently just has a badge.service I wrote to assign a badge to each reader based on how many minutes they've read. I'll scroll down just a little, and give you a peak inside the data.ts file. This is the file that contains the hardcoded client-side data currently being used in the app. It exports an array of Reader objects named allReaders, and an array of Books named allBooks. This data is just here to provide some minimum functionality in the app until we start adding our HTTP calls. In the next course module, I'll start updating the app to get data from the server. Let's now take a look inside the package.json file in the project. I'll close the sidebar so we can see it a little better. The primary thing I want to show you in here is the start script inside the scripts section at the top of the file. This is the script I'll use to build the app and start the node web server. I'll open the terminal built into Visual Studio Code, and show you how I run it. The server is actually already running, so I'll press Ctrl+C to stop it. I can then restart it with the start script above using the command npm start. You can see that it starts the web server listening on port 3000. It then builds the Angular app in watch mode, so that any changes I make to the code will automatically trigger a new build. That will make it easy for me to make changes and quickly see them just by refreshing my browser. Once everything is built, I can hop back over to my browser. I change the address to localhost:3000, and you can see that it automatically routes me over to the dashboard component, and everything still works like it did before. Okay, before I start writing any new code, I want to take just a minute, and talk about how RxJS fits into Angular apps. I'll do that in the next clip.
The Role of RxJS
Demo: Preparing to Use HttpClient in a Project
In this demo, I'm going to show you how to import the HttpClient module, and inject the HttpClient into an Angular service, so that your app is ready to make HTTP requests. Before I show you how to get the app ready to make HTTP requests, I want to use the Angular documentation to make it clear which HttpClient I'll be using. I'm here on the official Angular homepage at angular.io. I'll click the DOCS link at the top of the page, and then click the API link from the menu on the left. From here, you can filter or search for lots of different items available in the API. I'm going to use the Filter box, and search for the string http. The first two sections that show results contain all of the newer HTTP APIs that were added in version 4.3. These are what I'll be working with in this course. They're all the items in common/http and common/http/testing. Below that are two more sections, http and http/testing. Those sections contain the deprecated HTTP APIs. Okay, I'll now jump over to my editor, and get the app ready to make http requests. I'm going to start in the app.module.ts file. This is where I'll import the new HttpClientModule. I'll add an import statement at the top of the file to import HttpClientModule from @angular/common/http. I'll then scroll down to the NgModule decorator, and add the HttpClinetModule to the list of modules in the imports array on the decorator. This effectively makes the module available everywhere in the app. I'm going to make all of my HTTP requests from inside the data service I've already created, so I'll open it next. It's inside the core folder in the project. Since I already imported the HttpClientModule, the only thing I need to import here is the HttpClient class. It's also found in the angular/common/http module. That makes the class available in this file, but to get an actual instance of it I can use to make requests, I need to inject one into the services constructor. I'll do that by adding a parameter to the constructor. I'll name it http, and specify its type to be the HttpClient class I just imported. By including the private keyword in front of the parameter declaration, I can now use the parameter as a private class member throughout the class. To quickly demonstrate that, I'll jump down inside the getAllReaders method, and reference the new member by typing this.http. I'll type another ., and you can see I'm now getting code completion help showing me the methods that exist on the HttpClient. We'll start to use some of these in the next module to make requests to the server. I'll delete this for now. As I mentioned earlier, many of the methods in the HttpClient return observables, and observables are part of the RxJS library. So that I can work with those observables in this file, I'm going to add another import statement at the top that imports observable from rxjs/Observable. Okay, you've now seen how to import the HttpClientModule and inject the HttpClient into an Angular service. That's really everything we need to get started using the newer HTTP APIs. In the next module, we'll dive into the details, and start making requests to the server. Stay tuned.
Consuming REST Services
Introduction and Overview
In this module, we're going to tackle one of the most common tasks you'll probably perform in your Angular apps, Consuming a REST Service. Most modern client applications require data, and REST services are the most popular way of delivering that data. Don't worry if you're unfamiliar with what makes a service a REST service, I'll quickly cover that in the next clip. After that, I'll show you how to consume a REST service using Angular's HttpClient and observables. Along the way, we'll take a brief detour, and I'll show you a couple of powerful techniques for manipulating data using RxJS operators. There are lots of demos and lots of code to write, so let's get started.
What is a REST Service?
Let's quickly go over what a REST service is to provide some context for the code and techniques you'll see in the remainder of this module. The term REST is really an acronym, it's short for Representational State Transfer. The general idea is that you'll be transferring resources, which are just objects to and from the server, and taking advantage of the inherent features of the HTTP protocol to specify what should be done with those resources. You may often hear REST services referred to as a WEB API or an HTTP API. This is because they're so often used as the service architecture for web services that operate over the HTTP Protocol. REST services take advantage of different HTTP verbs to specify CRUD operations that should be performed on the server. CRUD is just another acronym that stands for create, read, update, and delete, the most fundamental ways to manipulate data. REST services work with resources, and there are conventions applied to URLs to allow you to address individual resources, as well as collections of resources. I'll show you how to do both in upcoming demos. REST services also make use of HttpResponse codes to indicate the success or failure of the requested action on the server. You're probably already familiar with the HTTP 200 response letting you know everything worked as expected. REST services report success with a little more granularity by using additional codes in the 200 range. As I mentioned a moment ago, CRUD is an acronym that describes the most fundamental ways to manipulate data, and they're what I'm going to focus on in this module, so I want to show you how those operations map to the HTTP features in a RESTful service. The C in CRUD stands for create. When you want to create a new resource on the server, which usually means inserting a row in a database, you send the request to the server using the HTTP POST verb. Angular makes this easy by including a helper method on the HttpClient named POST. Similar helper methods exist for the other HTTP verbs we'll use. The URL should represent the collection to which you'll add the resource. In this example, the URL is api/books, so we know we'll be inserting a new book. The new resource should be passed to the server in the body of the HTTP request. By convention, if that operation is successful, the server will return the HTTP status code 201 Created, and the newly-created resource will be returned in the body of the HttpResponse. The R in CRUD stands for read, and you read data from a REST service by using the GET HTTP verb. You can get an entire collection of data by just addressing the collection in the URL like the first example here, api/books. You retrieve an individual resource by adding another URL segment that contains the ID of the resource. The second URL here will retrieve the book with ID 5. Successful GET requests should return the HTTP 200 status code we're all familiar with. The U in CRUD stands for update. You update data by submitting a request with the PUT HTTP verb. The URL should address the specific resource being updated, and the body of the request should contain the resource itself, usually in JSON format. If the update is successful, the server should return the HTTP status code 204 No Content. In order to delete data, you submit an HTTP request with the DELETE verb, and use the URL to address the specific resource you want to delete. If the deletion is successful, the server should return 204 No Content just like the UPDATE operation. Okay, there's more to REST services than what I've covered on the last couple of slides, but these are the most commonly-implemented features and the ones most critical to supporting basic data operations. Let's now see the role observables play when using Angular's HttpClient to interact with REST services.
Subscribing to Observables
The methods that exist on Angular's HttpClient class that you'll most often use to interact with REST services all return observables. So before we get into the demos, let's take a quick look at how to work with those observables. Observables are really the most fundamental construct in RxJS, and they represent a set of values that can be delivered over time. In the case of HTTP requests, that value is usually just the HttpResponse. In this code, I've defined a method named getAllBooks. It contains one line of code that calls the get method on the HttpClient. The get method creates a HTTP request with the get HTTP verb. The URL it will request is /api/books, which will return an array of books in JSON format. That JSON will automatically be deserialized into an array of book objects as specified by the generic parameter I'm passing to the get method. The get method returns an observable that wraps up the data returned from the server. We also use a generic parameter to specify the type of data wrapped in the returned observable, which in this case is the same type of data returned from the server. I can call this function in some other part of the app just like I would call any other function, but because it returns an observable, I need to handle the response a little differently. In order to actually receive the results from an observable, you must subscribe to the observable. That's done by calling a method named subscribe. Rather than storing the returned observable in a variable and calling methods on the variable, it's common to just chain calls onto the function that returns the observable. The results from the observable are handled by callback functions passed to the subscribe function. It takes three optional functions as parameters. The first is the function that will contain data if the call executed successfully. I'm using an arrow function here. The function takes one parameter, which I've named data. Notice that the type of that parameter is an array of Books. This matches the type of the generic parameter added to the observable returned from the getAllBooks method. The body of the arrow function just assigns the data to the allBooks property on the component. The second callback function you can pass to subscribe handles errors. It's passed one parameter, which is the error, and here I'm just logging it. We'll spend a lot more time on error handling in the next module. The third callback function will execute when the observable is done, providing all of the data it's going to provide. This is basically a completion handler. It takes no parameters and is a good place for you to do any work you want to do once you know the observable is done. Okay, now that you know what REST services are and the basics of working with the observables returned from Angular's HttpClient, let's get into some demos.
Demo: Retrieving a Collection
Demo: Retrieving a Single Item
In this demo, I'm going to update the demo app to request a single book from the REST service using the bookID. I'm here in the dataService again, and this time I'm going to update the getBookbyId method here at the bottom of the file. It currently takes the hardcoded array of books I imported at the top of the file, and uses the find method to return the book containing the ID passed to the method. I'm going to remove that code, and have it make a call to the REST service instead. This is going to look very similar to the code in the getAllBooks code above. I'm again going to call the get method on the HttpClient, but this time the generic parameter I passed to it will just be Book instead of an array of books. I'm going to use a template string to specify the URL for the request. The request will still go to the books collection on the server, but I'll append the ID passed to the method as the last segment of the URL. We learned in the last demo that the get method returns an observable, and I'm just returning that observable from my method, so I need to update the return type to an observable of type Book. This code is called from the edit-book.component, so I'll now open it up. You can see that I've got an error in the ngOnInit method, because the return type of getBookById changed. I'll remove that assignment, and then subscribe to the observable returned from the method just like I did in the last demo. The assignment to the selected book property that I just deleted is effectively being moved to the first callback function passed to the subscribe method. If the call was successful, the book will be passed to this callback as a parameter, and I'll then assign it to the selected book property on the component. The second callback handles errors. I'll again just log it to the console. We'll add some more elaborate error handling in the next module. Remember that the third parameter to the subscribe function is a completion handler. It doesn't take any parameters. All of these are optional, and I find that I rarely have a need for a completion handler, so I think I'll just leave it off this time. Okay, I've changed the dataService to make a call to the REST service for a single book, and I've updated the EditBookComponent to subscribe to the returned observable. Let's now hop over to my browser and test it out. I'll refresh the app, and it looks like the dashboard still loads fine. Notice as I hover over the Edit links for the books, the URL associated with each link. The app will route to the EditBookComponent, and treat the last segment of the URL as a parameter that is the ID of the book. The component captures that ID, and then passes it as the parameter to the getBookById method on the dataService. I'll click the Edit link beside Winnie-the-Pooh, which has an ID of 2. At the bottom of the Network tab in the developer tools, we can see the request it made to the REST service. The URL was /api/books/2. I'll click on it, and we can see the data that was returned in the body of the HttpResponse, as well as the successful HTTP status code. Just to quickly demonstrate the error handler I passed as the second parameter to the subscribe method, I'll change the URL to look for bookID 300, which doesn't really exist. You can see in the Network tab at the bottom that the request returned status code 404. I'll go back to the browser console, and you can also see that the error was logged there by the callback function I passed to subscribe. Okay, let's now go back to the code, and tweak it slightly to pass some specific HTTP headers to the server. The server-side route is only coded to return JSON data, but it's common for services to have the ability to return data in multiple formats. In those cases, it's up to the client to pass an HTTP header to tell the service what data format it would like to receive. HTTP headers are also used to pass other useful bits of data to the server, so it's important to understand how to configure them when using Angular's HttpClient. I'll go back to the dataService, and we'll add some more code to the getBookById method. I'll declare a new variable named getHeaders, because it will store the headers I want to add to this get request. Its type will be HttpHeaders, and I'll initialize it to a new instance of that class. Visual Studio Code doesn't seem to recognize the type, but that's just because I haven't imported it into this file yet. I'll put my cursor over the error, and then use this little quick fix light bulb to Add HttpHeaders to the existing import declaration from @angular/common/http. The error then goes away, and if I quickly scroll back up to the top of the file, you can see that HttpHeaders is now being imported from the same module as Angular's HttpClient. Okay, there are several ways I can specify the headers I want to use, but I think the easiest to implement and read is to just pass an object literal to the HttpHeaders constructor. I'll add a pair of curly braces, and then I can just include the headers I want as properties on this object literal. I want to tell the server that I want JSON data returned, so I'll add an Accept header, and give it the value application/json. If I want to add additional headers, I can just add a comma, and then include as many more as I want. I don't have any security on my service, but just for demonstration purposes, I'll add an Authorization header, and set its value to my-token. This will create and store these headers in the getHeaders variable. I now need to add them to actual HTTP request. The get method I'm calling on the HttpClient can also accept a second parameter containing options you want to apply to the request. It's an object literal, so I'll add a comma after the URL parameter, and then a pair of curly braces. There are several properties you can specify on this object. To set the headers, you simply use the headers property. I'll assign it the variable I declared above containing the Accept and Authorization headers. Let's now go back to the browser, and see if the new headers are being included with the request. I'll refresh again, and go over to the Network tab in the developer tools. I'll click the Edit link beside one of the books, and then click on the request that generated on the Network tab. I'll scroll down just a little bit to the Request Headers section, and you can see that the request was sent to the server with an Accept header requesting JSON data, and an Authorization header set to my-token. The code I wrote to do this obviously works fine, but I made it a little more verbose than necessary for the sake of clarity. Rather than assigning headers to a variable, and then using that variable to attach the headers to the request, you could just create and assign the headers right on the request. I'll just copy this code and paste it over the variable I used on the Options object. I can now get rid of the variable above. I think I like this form a little better, but feel free to use whichever style you prefer. We're now successfully retrieving a collection of books and single books from the REST service. In the next couple of clips, we'll take a slight detour, and see how we can use RxJS operators to transform the data being returned from the server.
Using RxJS Operators
I want to take just a few minutes and talk about how you might take advantage of RxJS operators when working with the observables returned from your HTTP request. I'm not going to go into great detail in this course, but I do think it's important that you're at least aware of some of the capabilities they offer. Operators are really just bits of code that operate on observables and return an observable. Because they return observables, you can chain multiple operators together to perform complex transformations on your data before returning it to your components. This is really the power of operators as they relate to HTTP requests specifically. They give you the flexibility to transform your data into exactly the shape you need it, which can be very helpful when working with third-party services that may not return data in exactly the format you would prefer.
Demo: Transforming Data with RxJS
In this demo, I'll show you a simple example of using RxJS operators to transform data before returning it to the calling component. Let's imagine in this demo that the service where we're getting the books is actually a third-party service, and we really can't control the shape of the data being returned. However, we want to work with the data in our app in a slightly different form than what we're getting from the service. The first thing I'm going to add is a new class to the models folder in the project. I'll name it oldBook.ts. Inside it, I'll export a new class named OldBook that will be very similar to the Book class we've been using. I'll give this class a bookTitle property and a year property. We're going to pretend in this demo that this is the format we would prefer to use for our book data. I'll now open up the data.service again, and write some code to request a book from the server, but transform it into an oldBook instance before returning it to the component. I first need to import the oldBook class I just created. I'm going to use a couple of RxJS operators to transform the object returned from the server, so I need to import them as well. I'll start with just the map and tap operators. I'll then come down to the end of the file, and write a new method named getOldBookById that will be similar to the getBookById method just above it. Rather than returning an observable that wraps up a book, this method will return an observable that wraps up an old book. The request to the server will be the same. It only has books, so I'll copy the call to the get method from the getBookById method above, and paste it into this new method. I'll leave off the headers for now. The get method will return an observable, and I can apply operators to observables. I'm going to use the pipeable operator syntax released with version 5.5 of RxJS. You may also see these referred to as leadable operators. I just chain on a call to the pipe method, and then I can pass to it a comma-separated list of operators I want to apply to the object returned from the server. One of the most useful operators in RxJS is the map operator. You pass it a function that will accept the type currently wrapped in the observable, and return the data transformed in some way. I'm going to use an arrow function. The parameter b here will be the book instance returned from the server. I'm going to map its data into an oldBook instance. The bookTitle property on the oldBook will be assigned the title property from the book returned from the server, and the year property will get the value of the publication year property on the book. I'll add one more operator just for demonstration purposes. The tap operator doesn't transform the data, but just gives you a way to execute some bit of code before the final observable is returned. Because the map operator returns an observable that wraps up an oldBook, the function passed to the tap operator will receive that oldBook as a parameter. I'll name that parameter classicBook. I'll just log it to the console. Just to further make the point that all of our returned values match, I'll hover over the call to map, and you can see that it returns an observable. It wraps to the next line, but it's an observable of oldBook. I'll also hover over the call to tap, and you can see that it also returns an observable of oldBook, which matches the return type I gave the getOldBookById method. In order to test it, I need to actually call getOldBookById. I'll do that in the edit-book.component. I'll add an import statement to import the OldBook class. I'll then add some code to the end of the ngOnInit method. I'll call the getOldBookById method on the dataService, and subscribe to the observable it returns. The success handler will receive an oldBook instance as a parameter. I'll have it log the title of the book with a template string. I'm not going to bother with the other callback functions I can pass to subscribe right now. We're now ready to test it out. I'll go back to the browser, and refresh the app. I'll click the Edit link beside one of the books. The correct book is loaded on the page, but we can also see the message in the console that logged the title of the book after it was transformed to an oldBook instance using the RxJS operators. This isn't a course on RxJS, but I just wanted to give you a taste of the power operators offer when you need to transform and manipulate the data in your observables. Okay, in the next demo, I'll show you how to create, update, and delete data with the REST service.
Demo: Creating, Updating, and Deleting Data
In this demo, I'm going to add the remaining CRUD operations for books that will allow us to create, update, and delete them. The first thing I want to do is add the code to the dataService that will make the request to the server to insert, update, and delete books. I'll add a separate method at the bottom of the file for each operation. I'll name the first one addBook, and it will take a Book object as a parameter. The return type is going to be an Observable of Book. I'm going to add the book by making an HTTP request with the post verb. I can do that by using the post method available on the HttpClient. By convention, when a resource is added to a REST service with a post request, the newly-added resource is returned in the body of the HttpResponse. That object will also be the object that gets wrapped into the observable returned by Angular's HttpClient. Therefore, I'm going to specify the type of that object, Book, as the generic parameter on the method. I'm also returning from this method the same observable returned by the post method, which is why the return type of addBook is an Observable of Book. Just like the get method we've already seen, the first parameter to post is the URL where the request will be sent. I'm adding a book, so the URL is the same address I used when retrieving all books, /api/books. The server will interpret the two requests differently because of the different HTTP verbs being used. The second parameter I'll pass is the book object to be added, which is the newBook parameter passed into this method. The third parameter is an options object. I'm again going to use the Headers property of it to add a header to the request. I'll set the Content-Type header to application/json to let the server know that the content in the body of the request contains JSON data. The code to update a book is very similar to this, so I'll just copy this entire method, and use it as a starting point. I'll name this one updateBook, and I'll rename the parameter updatedBook. Updates to a resource are made by sending a request to the service with the put verb, so I'll use the put method on the HttpClient, which will make sure that verb is used. By convention, nothing is returned in the body of the HttpResponse when a resource is successfully updated. Therefore, I'm going to change the generic type parameter from book to void. I'll need to make the same update to the return type for the method. The URL is slightly different for updates, it should include the ID of the resource to be updated as the last URL segment, so I'll change this to a template string, and pass in the bookID of the book being updated. The book I'm passing to the server is the one in the updatedBook parameter. I'll leave this same header on the put request. I'll now write a method to delete a book, it's much simpler. I'll name it deleteBook, and it will just take as a parameter the ID of the book to be deleted. It will also not return anything in the body of the HttpResponse, so I'll set the return type of this method to be an Observable of void just like the updateBook method above. To delete a resource, you submit a request using the http.delete verb, which as you might expect, I can do with another handy helper method on the HttpClient. I'll pass void as the type parameter. The URL will be the books collection we've been using on the server with the ID of the book to be deleted passed as the last URL segment. I don't need any special headers on this request. Okay, I've now got methods on the dataService for add, update, and delete. I now need to update the components that will call these methods. I'll go to the addBook component first. I'm not currently using the dataService in this component, so the first thing I need to do is import it at the top of the file. I'll then inject it into the constructor, and store it in a private property named dataService. A little further down in the file, you can see that I've already got a method on the component named saveBook. It gets called when a user clicks the save book on the addBook screen. The form values are passed into the method and then casted to a book instance. I know it's a new book, so I then set its ID to 0. The server will give it a real ID as part of the save process. I then just log a warning that this method isn't fully implemented yet. I'll delete that, and then I can make my call to the dataService. I'll call the addBook method I wrote a minute ago, and pass in the newBook. We know that this method will return an observable, so I now need to subscribe to it. I'll call the subscribe method, and then write the function that will execute if the book is added successfully. Remember that I mentioned the newly-added book will be returned in the HttpResponse. That book will be passed to this callback function as a parameter. I'll just log it to the console. This will give us a chance to see how it may differ from the Book object that was passed up in the request. I'm logging the book that was sent in the request here, and I'm logging the request that came back in the response here. If there's an error, I'll just log it to the console. I'm not going to add a completion handler right now. That's all I need to do to do the addBook component. Let's now move on to the editBook component. I'll close the sidebar to give myself a little more room in this file. This component already has the dataService injected into it, and I've already got a saveChanges method that gets called when a user clicks the Save button on the Update screen. I'll delete this warning, and use a code snippet to paste in the code to call the dataService. This code is very similar to the code I just added to the addBook component. The current book is passed to the updateBook method on the dataService. Remember that no book is returned in the HttpResponse, so I just log a message using the title I already have. I'll also just log any error I get, just as I've done before. That's it for editing books. Deleting books is handled on the dashboard.component. There's a Delete link beside each book on the dashboard that will call this deleteBook method on the component, passing in the ID of the book to be deleted. I'll get rid of this warning that the feature isn't implemented yet. I'll then use another code snippet to paste in the new body of the function so you don't have to watch me type so much. This should be looking pretty familiar by now. I call the deleteBook method I wrote on the dataService, passing it the ID of the book to be deleted. I then subscribe to the returned observable. The first method passed to the subscribe method will not be passed any data, because nothing is returned in the body of the HttpResponse for a delete operation. However, I am doing just a bit of work in this function to remove the deleted book from the list of all books on the dashboard. I use the findIndex method that exists on the array of all books to find the index of the book with the ID of the one that was deleted. Once I have that index, I call the splice method on the array to remove the item at that index. The dashboard view that displays the contents of the allBooks array will automatically be updated to reflect the removal of the book. Below this, I'm again just logging any error that occurs. Okay, time to test out all these changes. I'll refresh the app, and then click the Add Book link at the top of the page. I'll add the book Corduroy by Don Freeman. I'll set the publication year to 2000, even though I know that's not correct. I'll fix it in just a minute. Before I click the Save button, I'll clear the request on the Network tab in the developer tools just to make things a little easier to read. I'll click the Save button, and we see the new HTTP request immediately appear down below. I'll click on it so we can see the details. We can see that the request was submitted to the books collection, and that the post verb was used. Just below that is the HttpResponse code. I mentioned on one of the slides earlier in the module that a successful post request should return 201 Created, which is what we see here. Remember that in the addBook component, I logged the book just before the HTTP request, as well as the book that came back in the HttpResponse. Over here in the Console, we can see the result of those log statements. Notice that the only difference in the objects is the value of the bookID. It was submitted to the server with an ID of 0, but the server assigned the book an ID when it was saved, and the book returned in the response has that ID of 7 on it. The ability to capture the new ID from the response is one of the reasons it's customary to return the newly-saved item after it's saved. Let's now test an update. I'll click the Book Tracker link at the top of the page to take me back to the dashboard. You can see here that Corduroy has been added to the list of All Books. I'll click the Edit link beside it to fix that incorrect publication date. Corduroy was actually published in 1968. Before saving that change, I'll go back to the Network tab in the developer tools, and again clear the list of requests. I'll click the Save button, and we see the new request appear below. You can see here the URL where it was sent. It used the HTTP PUT verb as expected, and because it was successful, it returned the HTTP 204 No Content response. That basically means that it was successful, but no data was returned in the body of the response. I'll hop over to the Console tab, and scroll down a bit, and you can see the log message that Corduroy was updated successfully. Let's now test the delete. I'll go back to the Network tab, and clear all the entries first. I'll then click the Delete link beside Corduroy on the dashboard. The HTTP request shows up below in the developer tools. We can again see that it went to the correct URL, used the DELETE verb, and because it was successful, returned 204 No Content. Also notice that the book was removed from the list of All Books on the dashboard. Remember that I did that in the success handler when I subscribed to the observable returned from the HTTP delete request. Okay, everything is working fine. The app is now been updated to creat, read, update, and delete book data using a REST web service. The service included with the demo app also supports all of the same operations for readers, so I would encourage you to practice these techniques by downloading the demo code and updating it to pass readers to and from the service as well.
That wraps up this module. If you weren't already familiar with them, you should now have a pretty good understanding of how REST services work, and more importantly how to communicate with them using Angular's HttpClient class. The combination of helpful shortcut methods, consistent use of observables, and the rich set of operators offered by RxJS make the HttpClient an ideal consumer of just about any RESTful service. In the next module, we're going to build on the code in this module, and learn more advanced techniques for handling errors and retrieving data during route transitions with resolvers.
Advanced HTTP Requests and Error Handling
Introduction and Overview
In this module, I'm going to build on the code from the last module, and show you how handle the inevitable errors you'll receive when retrieving data over HTTP. I'll also show you how to use a feature of Angular's router to prefetch data for your components before activating the new route. It's easy to build demo apps that are always fast and never do anything unexpected, but we all know this is not how software behaves in the real world. Any app you build that you plan to distribute to actual users needs to properly handle errors. I'm going to show you how to handle HTTP errors in your Angular app, and go one step further and show you how to abstract those HTTP details away from your components, and return custom errors. I'll then show you how to use resolvers to make sure a component has all of the data it needs before activating a new route. Let's start with the error handling.
Handling HTTP Errors
It's really no more difficult to handle HTTP errors than any other type of error. However, I think there are a couple of additional considerations when working with HTTP. It's a best practice to encapsulate HTTP errors in a service. I'm a strong proponent of making all HTTP calls from a service, so it's very natural to handle and encapsulate any errors in the same service. By doing that, you don't expose the implementation details of the service to the component. The component shouldn't know or care if the data was retrieved over HTTP or from some other storage mechanism. You can accomplish these two goals by using the RxJS catchError operator on the observables returned from the HttpClient. It will allow you to handle the error, and then return a custom error to the calling component. That will keep all of the HTTP details hidden inside the service, and give you a place to return a useful error that you can present to your users. Let's now go back to the demo app, and add some error handling to one of the HTTP requests I created in the last module.
Demo: Handling HTTP Errors
In this demo, I'm going to add error handling to an HTTP request, and use the RxJS catchError operator to return a custom error object to the calling component. I'll start in the dataService where I make all of the HTTP requests. I'll update the getAllBooks method to handle any errors returned from the server. Notice that it currently just returns the observable that's returned from the get method on the HttpClient. There's no error handling here right now. This method is called from the dashboard component, so I'll open it below this file. I'll try to rearrange this, so we can see all the interesting bits at once. Notice that I'm handling errors here when I call the subscribe method. The second callback function will handle any errors returned from the observable. Since that can really be anything, I've defined the type of the object being passed to the callback to have the any type. I'll update the code in the dataService to force an error, so we can see how all of this behaves. I've got a few endpoints in my server code that will return hardcoded HTTP errors that I use for testing things like this, so I'll change the URL I'm requesting to /api/errors/500. That will return an HTTP 500 Server Error. Since I'm not handling it in the dataService, it should fall all the way to the dashboard component and be handled there. I need to start up the app in my terminal with the command npm start. Once it's running, I'll switch over to my browser, and go to localhost:3000. Because I requested a URL that doesn't return any books, I obviously don't have any books show up on the dashboard. I'll open the developer tools, and take a look at the Console tab to see the error that was logged by the component. You can see here that the object logged is an HttpErrorResponse object. I'll expand it, and you can see that it has lots of HTTP-specific details in it, statusCode, statusText, URL, etc. Remember that this was logged by the component, so the HTTP details have now leaked out of the service making the request and into the component that called the service. Ideally those types of implementation details would stay in the service, and a more application-specific error object would be returned to the component. I'll go back to the code and make that change. The first thing I need to do is go back up to the top of the dataService, and add catchError to the list of the RxJS operators I want to use. I'm also going to import ErrorObservable from RxJS. I'll use it to send the new error object from the service back to the component. I'll now come back down to the getAllBooks method, and chain a call to the pipe method onto the observable returned from the call to get. This is the same technique I used with the RxJS operators in the last module. Here's where I'll use the catchError operator. It will be passed the error that occurred, and I'm just going to pass that error to a new method named handleHttpError. That will make it easy to handle other errors in this service with the same method. I now need to write this new method. We've already seen that the type of error thrown is an HttpErrorResponse, so that will be the type of the parameter passed to this method. It will return an observable that wraps up a BookTrackerError. I'll use the little light bulb in Visual Studio Code to import HttpResponseError into this file. BookTrackerError is an application-specific error class that exists in the models folder in my project. You can store anything you want on a custom error class like this. I'm keeping mine pretty simple with just an errorNumber, message, which will be a more technical error message, and a friendlyMessage property, that I could potentially display to users. I'll go back to the dataService, and use a code snippet to paste in the body of the handleHttpError function. The general idea here is that I want to take the HttpErrorResponse object that's passed in, and convert it into a BookTrackerError instance that can be returned to the component. I first instantiate a new BookTrackerError instance, and then assign values to its properties. I'm hardcoding the error number and using the statusText property of the HttpErroResponse as my technical error message. I'll also hardcode a slightly more user-friendly message. I still want this returned to the component as an error, so I'll return it by passing it to the create method on the ErrorObservable class. I've still got an error in the getAllBooks method above. I'll hover over it, and I'm told that an observable of BookArray or BookTrackerError is not assignable to Observable of Book array. Basically, I've added an additional type that might be returned in the observable, so that needs to be reflected in the function's return type. I'll update the generic parameter to also accommodate BookTrackerError. Now when this error callback passed to subscribe is called, it will be passed the BookTrackerError instance. Therefore, I can update the type of the parameter from any to BookTrackerError. I'll quickly import it in this file. Now that I've specified a type, I can log properties of that type, and get nice code completion support thanks to TypeScript. Okay, I'm now ready to go back to my browser and try out these changes. I'll refresh the page. I'm still getting the 500 Server Error as expected, but instead of a bunch of HTTP details being passed to the component and logged, you can see the user-friendly message I passed to the BookTrackerError is logged instead. This keeps those implementation details abstracted away from the component, and makes it easier to update the service later if I don't need to worry about the API it exposes to its callers. Before we move on to talking about resolvers, I'll hop back over to the code, and update the URL I'm using in the getAllBooks function. I don't want to keep getting 500 errors, so I'll change it back to api/books.
Retrieving Data with Resolvers
Let's now turn our attention to a routing feature known as resolvers. Even though this isn't a course on routing, resolvers are still important to discuss here, because they help you control precisely when your HTTP requests are made as part of a route transition from one component to another. Resolvers allow you to fetch data before navigating to a new route. So far in this course, all of the HTTP requests have been made after a new route was activated. Fetching data before that navigation happens has a couple of significant benefits. First, it allows you to prevent the presentation of an empty component. If the HTTP request is slow, the view for the new component might not be populated with all of the expected data when it's first shown to users. The second big benefit is that resolvers can prevent you from routing to a component with errors. If an HTTP request returns an error, there may be no point in continuing the transition to the new component. Resolvers allow you to handle that error, and redirect the user to a more appropriate component. Another potential benefit of resolvers is that they provide a better user experience, maybe. It's often the case that a component gets all of its data from a single HTTP request, and in those cases, I think resolvers almost always make for a better user experience. I qualified this bullet, because I think there are scenarios where you might legitimately decide that you can build a better user experience by not using resolvers, and letting the data show up in the view after the route transition. I'll show you an example of that in the next demo. Let me now give you a more visual representation of how resolvers let you control when HTTP requests are made. Let's first look at how data is retrieved during a route transition without resolvers. Imagine I have a component that presents some summary data. Clicking the Detail View button will route to a detail component, which will present some data retrieved from the server over HTTP. Without a resolver, as soon as the button is clicked, the new route will be activated. Activating the route will display the associated view, and trigger the HTTP request to the server for the detail data. The problem with this is that while the component waits on the data to be returned, the component's view is empty. Only when the HTTP request returns successfully does the data appear in the view. Let's now see how this same scenario would work with a resolver. If we start with the same summary component, clicking the Detail View button would trigger the resolver to make the HTTP request for the detailed data before activating the new route. Once that HTTP request returns with the data, the new route is activated, and the view is shown already populated with all of the expected data. If all of your HTTP requests execute really fast, you might not even notice the difference in the two approaches. However, we all know you can't assume your request will always be fast. In the next demo, I'll intentionally slow down one of the requests in the BookTracker app, and create a resolver to show you the effect it can have on the user experience.
Demo: Retrieving Data Over HTTP with Resolvers
In this demo, I'm going to create a resolver to prefetch some of the data displayed on the dashboard in the BookTracker app. In order to better demonstrate the effect resolvers can have on your apps, I've artificially slowed down the server-side code that returns the list of All Books. I just used the setTimeout function in my node app to have the server wait 2 seconds before returning the books. I'll refresh the app, so that you can see how it behaves in the browser. The Readers and Most Popular Book appear right away, but the list of books shows up a couple of seconds later. Let's imagine that I'm not really a fan of this behavior, and I would rather the view not appear until all of the data is available. To do that, I need to create a resolver. Resolvers are implemented as Angular services. I'm not going to go into great detail on services here, but I do go into great detail in them in my earlier Pluralsight course titled Angular Services, so check that out when you're done with this course. I'll create the new service in the core folder, and name the file books-resolver.service.ts. I'll use a code snippet to paste in some import statements at the top of the file. Most of these imports probably look familiar by now. The ones that may be new to you are these here on line 2. Since resolvers are really a feature of Angular's router, I'm importing a few things from the Router module. You'll see how I'm using them in just a second. I'll use another code snippet to stub out a new class for the resolver. Let's quickly walk through this code. Because this class is a service, I've added the Injectable decorator. I've named the class BooksResolverService, and specified that it implements the Resolve interface. This interface was one of the items I imported from the Router module. The interface takes a generic parameter that specifies the type of data that will be returned by the resolver. Since I'm going to use it to call the getAllBooks method on the dataService, I know that it will either return an array of books or a BookTrackerError. I'm then injecting the dataService into the constructor, so I can use it in the class. After that, I've got an empty method named resolve. This is a method that's defined on the resolve interface that this class must implement. In fact, it's the only method on that interface. You can see that it will be passed two parameters, an ActivatedRouteSnapshot and a RouterStateSnapshot, which were the other two types I imported from the Router module above. I'll collapse the explorer, and you can see that this function will return an observable that wraps up either an array of books or a BookTrackerError. The same union type I specified on the interface, I'm implementing. Resolvers can also be configured to work with promises or raw data, but since I'm working with HTTP, observables are what I'm going to use. I'm going to wire this resolver into my router configuration in just a minute, but basically what's going to happen is this resolve method is going to be executed for every route that uses this resolver. All I want it to do is call the getAllBooks method on the dataService, and return the observable that it returns. One thing you'll notice is that you won't see me ever subscribe to this observable like you saw me do in the last module. Angular will automatically subscribe to it, and pass along the resolved data as part of the route transition as we'll see shortly. I want to handle any returned from getAllBooks here, so I first need to import a couple of items from RxJS. I'll import the catchError operator, which I'll use to actually handle the errors, and I'll import the of method that lets me create a new observable that can be returned to the component. I'll chain a call to the RxJS pipe method onto the observable returns from getAllBooks, and use the catchError operator to handle any error that's returned. Inside that method, I'll just use the of method to return a new observable that wraps up the error. This will allow the error to be passed on to the component and displayed there. Another option you have here for dealing with the error would be to inject Angular's router, and route the user to another component entirely if an error is detected. Okay, I mentioned earlier that resolvers are just a specialized Angular service, and services must be provided in a module, so let's take care of that next. I'm going to provide this one in my core module. I'll first import the service class at the top of the file. I now need to add it to the providers array on the NgModule decorator. That's all I need to do to make it available as a service. In order to use the service as a resolver, I need to make a small change to my routing configuration. That's defined in the app-routing.module.ts file. I'll close the explorer to give myself a little more room. I only want to use the resolver on the dashboard route right now. To do that, I just add a resolve property to the object literal that defines the route. The value for the property will be another object literal. The properties on this object literal will receive the data from the resolvers. You can name the properties anything that makes sense. I'll name this one resolvedBooks. The value you assign to the property is the resolver that should be used to provide the data, BooksResolverService in this case. I'll again use the little light bulb to import the service. That's all I need to do to have the router serve up the book data to the component. However, I still need to update the component to receive the data, so I'll open it next. Okay, I'm currently using this chunk of code here to call getAllBooks on the data service, and process the data returned by subscribing to the observable. All of that is now being handled by the resolver. I'm calling a method on the dataService inside the resolver, and Angular takes care of subscribing to the observable, and delivering the data via the router. Therefore, I can delete all of this. In order to retrieve the book data from the router, I need to import ActivatedRoute from Angular's Router module. I also need to inject it into the component's constructor. I can now use that private route property to retrieve the book data. I'll assign it to a property named resolvedData. Remember that it will either be an array of books or a BookTrackerError object. The activated route has a property named snapshot, and it has a property named data. This is where Angular will pass in the data from the resolver. I provide the name of the property I specified on the router configuration inside square brackets. I'll close that so you can see it a little better. Since I decided to allow data and errors to float out of the resolver, I now need to check which I actually got. I'll use one more code snippet to paste in a simple if statement. This just checks the type of the resolvedData variable. If it's a BookTrackerError, then I log the friendly message property of the error object. Otherwise, I know I got the array of books I was hoping for, so I assign them to the allBooks property on the component. Okay, I'm now ready to try it out. The effect of using the resolver should be that the dashboard route isn't activated until the array of books are returned from my artificially-slow server call. Let's try it out. I'll refresh the app, and all the data shows up at once again; however, notice that we didn't see anything on the screen this time until the requests for the books completed. This is where I think you have to decide what technique provides the best experience for your users. If the component you're routing to only contains data for a single HTTP request, then I think it's probably best to use a resolver, and only route to it after you know the data is ready. I think it's less clear cut when a component uses multiple HTTP requests to get its data, like I'm doing on the dashboard. Maybe I shouldn't hold up the whole route transition just because one network call is slow. The users might prefer to immediately see the data that is available, and have the rest appear when it's ready. I think this is something that really has to be decided on a case by case basis.
In this module, you've seen how you can handle HTTP errors and keep HTTP implementation details hidden inside your services. I also showed you how to use resolvers and the effect they can have on the user experience. These are both important topics that go beyond learning the basic mechanics of making HTTP requests. I hope you've seen that they aren't terribly difficult to implement, and that they can greatly enhance the quality of your apps. In the next module, we're going to begin looking at interceptors. They're a powerful feature that lets you tinker with your HTTP requests and responses as they flow to and from the server.
Introduction and Overview
Welcome back. In this module, I'm going to cover one of my favorite HTTP features in Angular, interceptors. I think the reason I like them so much is that they allow you to write a small bit of code, configure it in a single place, and then have it applied to all of your HTTP requests and responses. I'm naturally kind of lazy, so anything that lets me do so much work with so little effort is instantly appealing. I'll first cover exactly what interceptors are, and how they fit with the HTTP communication techniques we've already seen. I'll then cover when they're useful and how to create them before adding a couple of them to the BookTracker app. Also note, this is just the first of two modules on interceptors. I'll cover the basics in this module, and in the next module, I'll show you how to cache HTTP requests in your app using interceptors. Let's get started.
What Are Interceptors?
Before I get into use cases and syntax, I want to try to describe more precisely what interceptors are. In terms of the Angular constructs you're already familiar with, they are just Angular services that implement a specific interface. That interface is named HttpInterceptor. If this pattern sounds familiar, it's probably because it's very similar to the resolvers I showed you in the last module. Resolvers are implemented as services that implement the resolve interface, and interceptors are services that implement the HttpInterceptor interface. Functionally, interceptors give you a way to manipulate HTTP requests before they leave the client on their way to the server. They also let you do the same thing with responses, letting you manipulate the response that came back from the server before it's handed over to the rest of your application. Let's imagine our client application here on the left and our server on the right. Interceptors effectively sit between the two. When you create a new HTTP request and send it to the server, the interceptor grabs it, manipulates it in some way, and then passes the new version of it onto the server. The same thing happens on the way back to the client. The server sends its request, which again flows through the interceptor on its way to the calling code. Just like it did with the request, the interceptor can manipulate the response and pass along the modified version of it to the calling code It's about now that I imagine you saying okay, I get it, but why would I do this? That's in the next clip.
Uses for Interceptors
When you're creating your HTTP request, you have a lot of freedom and control to customize them to look exactly how you want them to be sent to the server. The power of interceptors is that they give you one place to apply some of those custom bits, so that you don't have to do it over and over for every request in your application. This is great for things like adding specific HTTP headers to all of your requests. Add the header in an interceptor, and you don't have to remember or take the time to add it to every request. Logging is another excellent use case for interceptors. Maybe while in development, you want to dump the body of every response to the browser console, or maybe you want to examine the status code of every response, and send a separate message to the server any time you encounter an error. Interceptors are great for that. You can also use them to report progress events, which can provide a nice user experience during long-running requests. Client-side caching is another great use case. The calling code can make all of the request at once, and you can let the interceptor implement the logic to decide if the response should be provided from a cache, or whether the request should flow all the way to the server. I'll show you how to do that in the next module.
Defining and Providing Interceptors
Let's now look at the code required to create an interceptor. Much like the resolvers I showed you in the last module, interceptors are defined as a class that implements an interface. Resolvers implement the resolve interface, and as I said before, interceptors implement the HttpInterceptor interface. Just like the resolve interface, the HttpInterceptor interface only has one method. It's named intercept, and it takes a couple of parameters with types we haven't seen yet in the course. The first parameter has the type HttpRequest, and as you might expect, represents the request on its way to the server. You'll manipulate this parameter inside the intercept method before sending the newly-modified request onto the server. However, your application may use more than one interceptor, and that's where the second parameter comes in. It has the type HttpHandler and represents the next interceptor in the chain of interceptors or the HTTP client that will forward the final version of the request onto the server. This parameter is usually given the name next, since it represents the next stop in the interceptor chain. The method returns an observable that wraps up an HttpEvent object. For most ordinary Http requests, the HttpEvent will represent the HttpResponse object. However, that's not the only type of HttpEvent. There's actually an enumeration I'll show you shortly named HttpEventType that includes several additional events such as upload and download progress events and a few others. The first thing you want to do in the body of the intercept method is make a copy of the HTTP request that was passed in. You can do that with the clone method as you see here. You need to do this, because the request object is immutable. Therefore, the changes you want to make to it need to happen on a copy of the object, and then the copy can be forwarded to the next interceptor in the chain. Once you've made your changes to the request, you pass it to the next interceptor by calling the handle method on the next parameter that was passed to the method. The newly modified request is passed as a parameter to handle. The handle method will return an observable that wraps an HttpEvent, so we can just return it from the intercept method here. However, we also have the option to modify the response before it's given back to the application. You can do that by using RxJS operators, which give you access to the HttpEvent being returned. You can check the type of the event, and if it's an HttpResponse, make any changes you'd like before the response continues on to the next interceptor headed back to the app. I'll show you a more complete example of this in the upcoming demos. Interceptors are services, and services must be provided in an Angular module. Here you can see the NgModule metadata for an AppModule. Interceptors are provided a little differently than other services. Notice that the value passed to the provide property is HTTP_INTERCEPTORS. This is a token you'll import from Angular's HTTP module. Your app can have multiple interceptors, and you'll use this token for all of them. The useClass property is where you'll specify the name of the class containing the interceptor. The last property on the object literal, which you may never have seen before, is the multi property. It should be set to true for interceptors. This configures the provide token as an array of values to be injected by the dependency injection system. The array of values is the multiple interceptors you may choose to use. If you do have more interceptors, you just add another item to the providers array that looks nearly like the first one. The only difference is you use the name of the class containing the next interceptor for the useClass property. The interceptors will be applied to HTTP requests in the order that you provide them to the module. Let's now jump into some demos.
Demo: Creating an Interceptor
In this demo, I'm going to show you how to create a basic interceptor that will apply a specific HTTP header to all requests. Let's suppose I want to send a content type header along with all requests, letting the server know that I will be sending data in the JSON format. The first thing I'll do is create a new file for the interceptor. I'll add it in the core folder, and name the file add-header.interceptor.ts. I'll use a code snippet to paste in the imports I need at the top of the file. The new bits here are the HTTP types I showed you in the slides. We'll use all of these with interceptors, and notice that I'm importing them all from the same Angular HTTP module we've been using throughout the course. Because interceptors are services, I'll add the Injectable decorator to the class I'm going to create. I'll export the new class, and name it AddHeaderInterceptor. Remember that interceptors are just services that implement the HttpInterceptor interface. I'll put my cursor inside the interface name, and press Alt+F12 in Visual Studio Code to have a look at the type declaration for it. I would encourage you to look at the type declaration files for types that are new to you. There are often lots of great comments that can help you learn what the type is, and how to use it. We can see here that this interface is used to intercept an outgoing HttpRequest and optionally transform it or the response. A little further down, the comments even explain that typically an interceptor will return a result of a call to next.handle. I'll scroll down some, and we can see that this is a very simple interface. It only has one method on it named intercept. I can have Visual Studio code automatically stub out an implementation of the interface for me using the little quick fix light bulb. I'll get rid of this throw statement, and have the method first log something about the request, so we know when it's executing. I'll just put the name of the interceptor and append to at the URL on the request object that was passed in. HTTP requests are immutable, so I need to add the new header to a copy of the original request that gets passed to the interceptor. I'll declare a new variable that will have the same type as the request passed in. I'll assign it the result of calling the clone method that exists on the request object. I'll again press Alt+F12, and look at the declaration for the clone method. Right now I'm using the basic version that takes no parameters, and just returns a copy of the request. However, there's a very helpful overload that takes an object literal as a parameter. It lets you make many of the most common changes to a request that will then be reflected in the copy that's returned. The object literal has optional properties to set things like headers, parameters, the request body, and the URL. There's even a setHeaders property that lets me specify new headers without having to instantiate a new HttpHeaders instance. That's the property I'm going to use. I'll pass an object literal to the method, and then assign to the setHeaders property another object literal that contains the header I want to add. I'll add the Content-Type header, and set its value to application/json. I then pass this modified request onto the next interceptor if there is one, by passing it as a parameter to the handle method on the next HttpHandler that was passed in. I'll hover over the call to handle, and you can see that it returns the same observable type as the intercept method, so I can just return its return value here. I could also manipulate the HttpResponse here if I wanted to, but I'm going to save that for the next demo. Okay, because this is a service, I need to provide it in a module. I'll do that in the core.module.ts file. Interceptors are provided using the HTTP_INTERCEPTORS token I showed you in the slides. I'll import that from the Angular HTTP module. I also need to import my AddHeaderInterceptor class. I can now provide the interceptor with another entry in the providers array on the NgModule decorator. I'm going to use the object literal provider syntax. The value for the provide property will be the HTTP_INTERCEPTORS token. The useClass property gets the name of my interceptor class, and the multi property needs to be set to true, so that I can use multiple interceptors. Okay, that's it. Let's test it out. I'll start the app in the terminal with npm start. Once that's running, I'll switch to my browser, and go to the localhost: 3000. All of the data shows up on the dashboard as expected. I removed the artificially slow response with book data I used in the last module while demonstrating resolvers, so everything shows up quickly again. I'll open the developer tools, and we can see that the HTTP request for book data did pass through the interceptor. It logged the name of the interceptor and /api/books as the URL it requested. More importantly, let's see if it added the Content-Type header to the request. I can check that on the Network tab. I'll refresh again, so I can see all the requests. I'll click on the request for books, and then scroll down to the section for Request Headers. There it is, Content-Type: application/json. That will now be added to every request to the server, which lets me reuse that bit of code, and not have to remember to add it myself to every request. In the next demo, I'll create another interceptor, and show you how to manipulate the HttpResponse.
Demo: Intercepting Responses and Using Multiple Interceptors
In this demo, I'm going to show you how to intercept a response and how to provide and manipulate the order of interceptors. Because I want to show you how to work with multiple interceptors, I'm going to start by creating a new one in a file named log-response.interceptor.ts. I'll use a code snippet to paste in my import statements. These are nearly identical to the imports from the last demo with a couple of minor additions. I'm now importing HttpEventType, which is going to help me determine when the event being intercepted is an HttpResponse. I'm also importing the RxJS tap operator, which I'll use to manipulate the response. I'll step out the class with another code snippet. This is very much like the interceptor class in the last demo. I've named this one LogResponseInterceptor, but it implements the HttpInterceptor interface just like you saw before. I want to be able to tell when this interceptor is executing in relation to the addHeaderInterceptor I added earlier, so I'll add a console.log statement with the name of this interceptor and the URL being requested. If I were planning to manipulate the HttpRequest, this is where I would make a copy of the request and start making changes to it. I'm only interested in manipulating the response here though. Do note that I can manipulate the request and response in the same interceptor. I'm choosing to use two here for a couple of reasons. First I want to show you how to configure multiple interceptors, and second, the functionality provided by each of them is very different, so I'd rather keep them separate from each other. I'm not manipulating the request, so I can just pass the original request to the handle method. I'm then going to use the RxJS tap operator to give me access to the response. I'll chain on a call to the pipe method, and pass it the operator. The parameter passed to the operator will be the HttpEvent object wrapped in the observable being returned by the call to handle. There are multiple event types, so I need to do a check to make sure this is actually an HttpResponse event, before I start doing anything with it, I'll do that with a simple if block. HttpEvent instances have a property named type, I'll check to see if that type is the response type that exists on the HttpEventType enumeration I imported at the top of the file. If so, then I know I'm working with the response. I name this interceptor LogResponseInterceptor, so all I'm going to have it do is log a property of the response. Notice here the nice help I'm getting from TypeScript. Inside this if block, it knows that the type must be HttpResponse, so the code completion help only shows me properties that exist on that type. I think that's pretty cool. I'll log the body of the response. That's all I'm going to have this interceptor do. I now need to provide it in the core module just like I did the other one. The syntax is nearly identical to the other interceptor, so I'll just copy and paste it. The only change is the name of the class, which needs to be LogResponseInterceptor. I'll use the little light bulb to import that class. The order the interceptors are provided here matters. The AddHeaderInterceptor is provided first, so it will get the request first, and then pass it to the LogResponseInterceptor. Let's try it out. I'll refresh the app, and take a look at the Console. Look at the order of the log statements. We can tell that the addHeaderInterceptor executed first, followed by the LogResponseInterceptor. Below that, we can see that the LogResponseInterceptor did its job and logged the body of the response. I'll expand it, and you can see the array of books that came back from the server. Just to drive home the point that order matters, I'll go back to the code and switch the order of the interceptors in the providers array. LogResponse is now followed by AddHeader. I'm sure you can guess what this is going to look like. I'll refresh again, and this time you can see that the LogResponseInterceptor executed first, followed by the addHeaderInterceptor. This is important to keep in mind if you're using multiple interceptors, and they're all manipulating the request. They will each receive the request output by the previous interceptor, which may look very different than the original request generated by the app.
Interceptors let you do a lot of useful work with very little code. They're the perfect place to perform those small tasks you want to apply to all of your HTTP requests and responses. In this module, I showed you how to use them to add a header to all of your requests, and how to log all of your responses. In the next module, we'll continue working with interceptors, but dig a little deeper, and use them to implement basic client-side caching of our request. Rather than just being a handy time saver, this can actually improve the performance of your app, so keep watching.
Caching HTTP Requests with Interceptors
Introduction and Overview
In this module, we're going to build on the interceptor fundamentals I covered in the last module, and use them to implement client-side caching. In the last module, I explained to you what Angular interceptors are and how to implement them in your apps. I showed you a couple of useful, but admittedly simple cases when you might use them. In this module, I'm going to show you how to combine your understanding of Angular services and the interceptors to implement application-specific caching that can really improve the performance of your apps. There's no new syntax to learn in this module, so I'll primarily focus on how to put the pieces together that you already understand to form a useful solution.
Benefits and Types of Caching
Before I get into the implementation details for an interceptor cache, I want to talk briefly about the benefits of caching and a few different types of caches. The primary reason for implementing any sort of cache is to improve the performance of your app. That can manifest itself in several ways. Cached HTTP requests provide an HttpResponse without actually making a network call to the server. This reduces network traffic, and less traffic means less contention for limited bandwidth and less time spent waiting on calls to complete. Each request that can be returned from a cache is also one less request that must be processed by the backend servers. The result is a smaller load on your web servers, database servers, and any other resource that might be required to service a particular request. All of these benefits contribute to the most important performance improvement, and that's the improved responsiveness of your app that your end users will experience. Strategically adding caching to your app can just make the whole thing feel snappier. The two most common types of caching we web developers take advantage of are browser caching and server-side caching. Browsers are great at caching resources your site needs, but it can be cumbersome to control the behavior of the cache at a very granular level. Server-side caching can take some load off of your backend storage systems, and you can exert a lot of control over exactly how the cache is implemented. The drawback is that it's a server-side cache, and that means you still need a call to the server. I think implementing an application-specific client-side cache using the technique I'm going to show you gives you the best of both worlds. The cache is on the client, so your app remains responsive, and you have a lot of freedom to control exactly how it works in order to maximize the benefit it provides. In the next clip, I'll give you a high-level overview of how this can be implemented with interceptors.
Caching with Interceptors
I used a similar slide to this in the last module to explain how interceptors work. I've updated it slightly to show how an interceptor can facilitate caching. Imagine our client application here on the left and the server on the right. The interceptor and the cache conceptually sit between the two, even though we know they actually reside on the client. You saw in the last module how to implement an interceptor. The cache will be implemented as an Angular service that can maintain a list of the items we want to be cached. When the client makes an HTTP request, the interceptor will grab it, and check the cache to see if it already has a response for that request. If so, the interceptor retrieves it from the cache, and then sends it back to the client. The request never goes to the server. Let's now look at the alternative situation. A request is made from the client, the interceptor checks the cache, but does not find a valid response. The interceptor will then forward that request onto the server to be handled normally. When the server response comes back to the interceptor, it will store that response in the cache before sending it on down the line to the client. When that same request is made again later, it will be retrieved and returned to the client from the cache. Since the cache is just a service, you could really use it from anywhere; however, the great part about pairing it with an interceptor is that the code actually generating the HTTP request can continue to make as many requests as necessary, while all of the work to utilize and manage the cache is delegated to the interceptor. The Angular service that implements the cache obviously plays a pretty important role in this process. Its primary job is to provide a data structure that can store the cached items. I'm going to store each cached item as a property on an object. The service also needs a very simple interface for interacting with the data structure. It needs a method that lets you add items to the cache, retrieve items from the cache, and remove items from the cache. You may also hear this referred to as cache invalidation. It's really just the process of removing items from the cache, so that future requests for that item will be supplied from the server. In the next clip, I'll build a simple cache service that will store HTTP requests made by the BookTracker app.
Demo: Create a Service to Store Cached HTTP Requests
In this demo, I'm going to start implementing a caching solution by building the cache service that will store the HTTP request. The service will just be a very ordinary Angular service, so I'm going to use the Angular CLI to create it. I'll do that from the terminal in Visual Studio Code. I want to put it in the core folder, so I'll first change into that directory. The command I'm going to use is ng generate service, followed by the name I want to give the service. I'll call it HttpCache. I'll add the --module flag to have the service automatically provided in the core module. I don't want any unit test right now, so I'll set the spec option to false. You can see in the output that it created a new file for the service and updated the core module file. Let's take a quick peek inside the module. You can see here that the CLI added an import statement for the new service, and then added it to the end of the providers array in the module metadata. Let's now open the service file itself. It's pretty basic right now, but it's a good starting point for our caching service. I'll first import the HttpResponse class from Angular's HTTP module. That's the only additional thing I need to import right now. I need an object that can be used internally to store all of the cached items, so I'll declare a new private property named request. I'll declare that it has the any type, and initialize it with an empty object literal. I'm now going to write the methods that will make up the API for the cache. We obviously need a way to put things in the cache, so I'll start with that. I'll add a method named put that takes two parameters, the URL being requested and the HttpResponse that will be cached. I won't have this method return anything. There are lots of different ways you could create and manage the underlying data structure for something like this, but I'll store the cached items by adding properties to the request object using the URL as the property name. The response to be cached will be the value assigned to the dynamically-added property. That's all the code I need to add a new item to the cache. Now I need a way to get an item from the cache. For that, I'll add a method named get. It will take a URL as a parameter, and return either an HttpResponse object if one is found in the cache, or undefined in the case that there was nothing cached for that URL. All I have to do is return the value assigned to the property with the name of the URL passed in. If it exists, this will return the HttpResponse stored for that property. Otherwise, it will just return undefined. Okay, I now want to add a couple of methods to invalidate items in the cache. Imagine that a request for allBooks got cached, and then someone added a new book. The cached response for allBooks is now out of date, so I don't want to return it from the cache. I want the next request for allBooks to get a fresh copy from the server. These next two methods will let me remove items from the cache to support that kind of behavior. I'll name the first one invalidateUrl, and it will take a URL as a parameter. It will let me remove a single item from the cache, the HttpResponse stored for the URL being passed in. I'll just set the value for that property to be undefined. The last method I want to add will let me invalidate the entire cache with one method call. I'll name it invalidateCache. I'll have it simply assign a new empty object literal to the request property. That effectively gets rid of everything that was in the cache. Again, there are lots of ways to implement something like this, I'm keeping this one purposely pretty simple, so we can primarily stay focused on the important concepts I want you to understand. Okay, that gives us all the basic functionality we need. I can put items in the cache, get them back out, and remove one or all of them as necessary. In the next demo, I'll create a new interceptor that uses this service to manage the caching for the BookTracker app.
Demo: Create an Interceptor to Cache HTTP Requests
In this demo, I'll finish building the caching solution by adding the interceptor that will make calls to the cache service. I'll start by adding a new file for the interceptor. I'll name it cache.interceptor.ts. I'll use a couple of code snippets to add a lot of the basic interceptor code you saw in the last module. This one just adds all of the import statements I'll need. It's pulling in all of the HTTP classes I'll need, as well as the tap and of methods from RxJS. I'm also importing the HttpCacheService I created in the last demo. I'll use another code snippet to stub out the class itself. There's nothing here you haven't seen before, it's just the basic code for an interceptor. Do note that I'm injecting the HttpCacheService into the constructor, so I can use it in this class. I've also added a few comments, so you can go ahead and get a sense for the functionality I'm going to add to the interceptor. Let's now add some code to each of these comments. The first one says to pass along any non-cacheable request. I'm only going to cache requests submitted with the HTTP get verb, so I want to do a check here for that. If the request uses anything other than get, then I can effectively ignore it, and pass it on to the next interceptor in the chain. I'll use an if statement to check the method property of the incoming request. If it's not get, then I'll just pass the request to the handle method. I want to stop for just a minute, and talk about cache invalidation strategies. Deciding when to remove something from the cache and force the app to get a fresh copy from the server is completely dependent on the requirements for your specific application. You can make those rules as simple or as complex as you need to in order to provide the best experience for your users. I'm going to implement perhaps the simplest strategy possible here just to demonstrate the important concepts. I know that the BookTracker app only submits requests using the get, post, put, and delete HTTP verbs. I also know that any request using post, put, or delete is attempting to modify data in some way. Therefore, any request with one of those three verbs will potentially render the data in one or more of the cached responses invalid. I could check the specific URL in the request, and just invalidate the related responses, but that's more code than I want you to have to to be bothered with right now. Instead, I'm just going to invalidate the entire cache if the incoming request is not a get request. To do that, I'll just add a couple more lines of code inside this if block. I'll first log that I'm invalidating the cache, and output the HTTP verb and URL of the incoming request. I'll then call the InvalidateCache method I added to the HttpCacheService. That will remove everything from the cache. Since this bit of code now does a little more than just pass along the non-cacheable request, I'll update the comment to note that it also invalidates the cache. Okay, if the execution makes it down to the next comment, then we know we're dealing with a get request. The first thing I need to do in that case is retrieve any cached response associated with the incoming URL. I'll pass the incoming URL to the get method I wrote on the cache service, and assign the return value to a variable named cachedResponse. I now need to see if I got a valid response from the cache, and if so, return it. I'll use a code snippet for this bit. If I got a valid response from the cache, I first log a message to that effect, along with the URL associated with it. I then log the cached response itself. I want to show you that it looks just like the response that would come back from the server. Finally, I wrap it inside an observable with the RxJS of method, and return it to the calling code. One important thing to note here is that I'm returning a value without calling next.handle as you've seen me do in the other interceptors I've written. You're not required to pass the request along to the next interceptor if you already have the value you want to return as I do here. Okay, if I didn't get a valid response from the cache, I need to send the request to the server, and then add the subsequent response to the cache. I'll use another snippet for that. I send it to the server by passing the request to next.handle. I'm then processing the response using the same technique you saw me demonstrate in the last module. I use the RxJS tap operator, and see if the HTTP event passed in is an HttpResponse. If so, I log that I'm adding a new item to the cache, and then pass the URL and the response to the put method on the cacheService. The response is also returned from the interceptor and makes its way back to the calling code. That's all the code I need in the interceptor. I now need to provide it in a module. I'll open the core module again, and import the new interceptor class. I'll then copy and paste one of the other interceptors in the providers array and update the copy to use the CacheInterceptor class. I'm now ready to test it out. I'll start the app in the terminal. Once it's running, I'll again go to my browser and open localhost:3000. I'll open the developer tools, and we can see the request that went to the server to retrieve all of the books right here. I'll take a look at the console, and we can see that it contains a log message reporting that an item was added to the cache. It's the response for the request to /api/books. Before I do anything else, I'm going to clear the console, so we don't get confused about the messages we see here. I'll also go back and clear the Network tab as well. I'll now click on the Add Book link just as a way to navigate away from the dashboard. Clicking the Book Tracker link to return to the dashboard loads all the books successfully, but notice that there are no new requests to the server down below in the developer tools. Back on the Console tab, there's a message that a cached response was returned for api/books. Remember that I also logged the cachedResponse, and that's what you see below this message. It came from the cache, but notice that it's still an HttpResponse object exactly like the one that originally came from the server when the app loaded. Okay, let's now test invalidating the cache. I'll clear the console again before doing that. Remember that my simple invalidation rules require sending a post, put, or delete request. I'll add a new book, which will generate a post request. I'll add the book Corduroy by Don Freeman, which I deleted earlier in the course. I'll click the Save button, which generates the request, and you can see that one of the log messages down below reports that the cache was invalidated as a result of a post request to /api/books. When the next request is made for all books, it should go all the way to the server again, since they won't be found in the cache. I'll clear the console again, and test that by going back to the dashboard. All of the books show up, including Corduroy, and a message in the console reports that the response with the books was again added to the cache. On the Network tab, I can verify that a request did in fact go all the way to the server. The first of these requests was the post that added the new book. The second is the request that retrieved all of them when I returned to the dashboard. Looks like it's all working as expected.
I told you earlier than interceptors are one of my favorite Angular HTTP features, and I think that caching is one of my favorite ways to take advantage of interceptors. With very little code, you can strategically cache requests, which reduce the load on your servers, and more importantly provide a better experience for your users. I hope you'll give them a try in your own apps. In the next module, we're going to shift gears a little bit, and talk about testing your HTTP requests. You do write unit tests, right? Okay, good. Well, after watching the next module, you'll have no excuse not to test those HTTP requests as well.
Testing HTTP Requests
Introduction and Overview
In this final module of the course, I'm going to show you how to get started writing unit tests for the HTTP requests in your apps. I think it's pretty well accepted these days that writing unit tests increase the quality of your code, and that their benefits are well worth the extra development time they require. The Angular team obviously feels the same way, because they've built special testing tools not just for HTTP requests, but for the entire framework. They've also made sure that new Angular apps created with the Angular CLI already have all of the external testing tools you need installed, so that you can easily write tests right from the very beginning. I'm not going to cover all of the ins and outs of unit testing in general or even Angular unit testing, but I will show you how to get started writing very useful unit tests for your HTTP requests that will hopefully help you identify problems in your code before you get a late-night phone call reporting the problem in production.
Angular's Default Unit Testing Tools
Structure of Angular HTTP Unit Tests
I mentioned earlier that Jasmine's beforeEach function is where you should put any setup code that you want to run before each of your unit tests. Because each test needs to start fresh, this is where you should configure an Angular test environment. Another test utility is the TestBed class. It configures an environment for running your Angular unit test. It also implements Angular's injector interface, which means you can use it to inject the classes you need into your test. Here I'm calling the configureTestingModule function on it, and passing it an object literal. Since I'm configuring a module, the properties on the object look a lot like the properties you normally provide to the ngModule decorator on your own modules. I use an imports property to import the HttpClientTestingModule, which will provide the HTTP backend for my test. I also use a providers property to specify the services I need provided here. This will make sure I can get an instance of the service I want to test from the module's injector. That's the other thing you normally do inside of a forEach function, retrieve instances of the service you want to test and an instance of the HttpTestingController I showed you earlier. Because the test bed implements Angular's injector interface, I can use the get method on it to pass it a type in order to get an instance of that type from the injector. Once I have an instance of the service I want to test and an HttpTestingController instance, I'm ready to start writing my test. Remember that your tests are written inside an it function. This code will test that my service returns the correct book. The first thing I'll have it do is execute the application code I want to test. This will test the getBookById method on the service, and pass it Id 2. That method returns an observable, so just like in my controllers, I need to call subscribe to make sure that the request actually happens. These success callback will be passed the data from the response, and it's inside that callback that I can make some assertions about that data, to make sure it's what I expect. The callback executes asynchronously, so it's only going to get called after we do some more work below to mock the request and send it a response. We use the httpTestingController to get the mocked request. You do that by calling a method on it, and passing it search criteria, so it can find the request made by your code that it needs to mock. I'm calling the expectOne method, and passing it the URL /api/books/2. This tells the controller that there should have been exactly one request made with a URL matching the one here. If that is not the case, an error will be thrown, and the test will fail. If a matching request is found, it will be mocked and assigned to the variable I've declared. You may then test various things about the request to make sure they match what you expect. This is a good place to check things like the HTTP verb being used, headers you expected to be passed, and other things stored on the request. You generate an HTTP response for the request by calling the flush method on it. You pass it the body of the response you want it to generate, and you can optionally pass it other things like headers and specific status codes. I don't have any of those on this request. This is the response that will then be processed by the success handler in the subscribe method above. Let's now go add some more detail to this, and write some real tests for the BookTracker app.
Demo: Testing HTTP Requests and Responses
In this demo, I'm going to create a new file to test the dataService, and show you how to write tests against HTTP requests and responses. I'll start by adding the new test file to the core folder. I'm going to be testing the dataService, so I'll name the file data.service.spec.ts. As I mentioned earlier, tests are known as specs, so the name of your test file should end with spec.ts. I'll paste in the import statements I need at the top of the file. We haven't seen these first two lines so far in the course. The first one imports the Angular test bed from angular/core/testing. It's one of the Angular testing utilities that helps create the proper Angular environment in which your tests can run. The next line imports the two classes I already talked about, HttpClientTestingModule and HttpTestingController, as well as a third class named TestRequest. These are being imported from angular/common/http/testing. It's also importing DataService and Book, since I'll also need them in the test. I'll use a code snippet to snub out the basic Jasmine functions I'll use. A suite of tests is defined by a function named describe. All of the actual tests will be created inside it. The string here is just a way to give a name to this suite of tests. Before I write any actual tests, I'm going to declare a couple of variables to store objects I'm going to need throughout the suite. I'm testing the dataService, so I'll need it just about everywhere. I'm also going to use an httpTestingController in each of the tests. I'm not initializing either of these variables here. I'll do that inside the beforeEach function. BeforeEach is the place where you should put initialization code that should run before each test. The first thing I'll do there is use the TestBed class I imported to configure the Angular testing environment. I'll call the configureTestingModule method, and pass it an object literal. As you saw me highlight on the slides, I'll use the imports property to import the HttpClientTestingModule, which will serve as the HTTP backend. I also need to provide the dataService, so I can receive an instance of it from this module's injector. Now I'm ready to assign values to the two variables I declared above. I'll get instances of them from the test bed by calling its get method and passing the type I want. I need a dataService instance and an HttpTestingController instance. Okay, what I have so far is really the basic setup you'll need to test HTTP requests regardless of what your app is designed to do. In order to actually write the test, you'll probably need some additional things that are specific to your app. The BookTracker app is all about books, so I need some books I can use in my test. I'll declare an array of hardcoded books with three books near the top of the describe method with the other variables. This gives me some data I can return in my mock HTTP responses. Now I'm ready to write some tests. I've already got two tests stubbed out at the bottom of the file. Tests go inside a call to the it method, and these two tests are just labeled test 1 and test 2 for now. I'll change the first one to state that the test should GET all books. I want to test the GET all books method on the dataService, so the first thing I'll do inside the test is call that method on the dataService instance I initialized above. I showed you earlier in the course that until you subscribe to the returned observable, the request won't be made, so I'll chain on a call to subscribe. I now need to retrieve a mock of the HTTP request made by the method. I'll declare a new variable to store it. The mock request returned from the testing controller have the type TestRequest. You generate a mock request by calling one of the methods on the HttpTestingController. The match method will return an array of requests that match a parameter passed to the method. ExpectNone takes a URL as a string and expects that no requests have been made with it. An error will be thrown if it finds one. I'm going to use expectOne. It also takes a URL as a parameter and expects to find that exactly one request has been made to the URL. Finding any more or less will throw an error. If I finds exactly one, then it will mock a version of the request. The request inside GET all books should go to /api/books. We can use the return TestRequest to make sure it looks like the type of requests our app was supposed to generate. I'll put my cursor on the TestRequest type, and press Alt+F12 to view the class definition for it. The comment on the class tells us that it's mock request that was received and is ready to be answered. It also gives us access to the actual HTTP request and methods that let you generate responses. The request property contains the underlying request, and a little further down, you can see the flush and error methods that let you send a response to the calling code. Back in my code, I'm going to test that the HTTP request made by GET all books used the GET HTTP verb. I'll use Jasmine's expect method to check the method property of the mock's underling request. I'll chain on a call to Jasmine's toEqual method to see if the method is GET, if it's not, this test will fail. The next thing I'll do is call the flush method, and pass it the array of testBooks I declared above. That will add the books to the body of the response sent to the calling code. I could also optionally set other properties on the response like the status code and headers, but I'm just going to set the body here. I can handle the response, and test how GET all books processed it inside the success handler passed to the subscribe method. We know that it will take an array of books as a parameter, and it should be the same array sent in the response from the server. Therefore, I can test that the length of the array is 3, since that's how many books were in my test array above. If the length of the array here is anything other than 3, the test will fail. The last thing you need to do at the end of every test is call the verify method on the TestingController. This tests that all HTTP requests have been handled, and that there are none pending. If there were, then that would also cause the test to fail. Okay, this test is now ready to run. I'm going to run it using an npm script that came with the package.json file created by the Angular CLI. Inside package.json, there is a script here named test. All it does is run the Angular CLI test command ng test. This will build the app, run all of the tests, and then it will sit and watch for changes to the code. When they're detected, the test will automatically be run again. I'll start it in the terminal with the command npm test. Once the app is built, the tests are run, and you can see the results here at the bottom, Executed 2 of 2 SUCCESS. The second test is still juts a stub I haven't added any code to yet. If you look closely at some of the other output above, you can see that Karma is attempted to launch a browser. In addition to reporting the test results here in the console, it will also post them to a web page in a browser window. I'll switch over to it just so you can see what it looks like. It's currently reporting that I have two specs and 0 failures. Below that, you can see the labels I used to describe the suite and the individual test. This page will also automatically refresh as the tests are rerun when code changes are detected. I'll show you the results in the console from here on just because it's a little quicker. Back in the editor, I'll make the terminal a little smaller so I can make some changes, but still keep an eye on the test results in it. Just so you can see what it looks like when a test fails, I'll go back to the code and change the expected HTTP verb from GET to POST. The file is auto-saved, which triggers the app to be rebuilt and the test rerun. The failure appears in the console. I'll scroll up just a bit, and you can see the message Expected GET to equal POST. To fix it, I'll just change the post back to get. Everything reruns, and now both tests are passing again.
Demo: Testing HTTP Errors
In this demo, I'm going to add another test to the dataService, and show you how to test HTTP errors. I'm going to add another test to the same spec file I worked on in the last demo. But before I do that, I'm going to open the dataService and refresh your memory on how I'm handling errors in it. Remember that in the GET all books method, I'm using the catchError operator from RxJS to process any HTTP errors that come back from the server. If I get an error, the handleHttpError function is called. It's defined right below. It takes the HttpErrorResponse object that was generated, and converts it into a BookTrackerError object with the errorNumber property set to 100, and the friendlyMessage property set to an error occurred retrieving data. It then wraps the object in an observable and returns it. So I think what I want to test in the spec file is that when an error occurs, it does properly convert it to a BookTracker error instance. I'll change the label on the second test to should return a BookTrackerError. It's going to call the same application code as the previous test, getAllBooks. I can mock the request with the same line of code I used before, so I'll just copy and paste it into this test. I'm really focused on how an error in the response gets processed, so I'm not going to test anything on the request right here. You can simulate a network error by calling the error method on the TestRequest object; however, to test normal HTTP errors, I think it's easier to use the same flush method I showed you in the last demo. The first parameter to flush is what should be sent in the body of the response. I don't really care about that here, so I'll just pass the string error. The second parameter is an object literal that lets you set some specific properties on the response. I'll mock a server error here by sending back a status code of 500 and statusText set to Server Error. That should trigger the error handling code I showed you a moment ago in the dataService. I need to test how all of this is handled inside the subscribe method. The first parameter is the success handler. I'm specifically trying to generate an error, so the success handler should never get called. If it does, I'll have it call Jasmine's fail method, which will cause the test to immediately fail. The second parameter to subscribe is the error handler. It should receive a BookTrackerError as a parameter. I haven't imported that type into this file yet, so I'll use the little quick fix light bulb to do that. Inside the body of the arrow function, I'm going to use the Jasmine expect function to test that the 500 Server Error got converted to a BookTrackerError object with the errorNumber property set to 100. I'll also test that the friendlyMessage property is correctly set to an error occurred retrieving data. The last thing I need to add to this test is a call to the verify method on the httpTestingController. I mentioned in the last demo that this should really be the last thing you do in all of your HTTP tests. Because of that, I'm going to take advantage of a Jasmine method that will run after each of my tests. It's very much like the beforeEach method I showed you in the last demo, but this one is cleverly named afterEach. I'll cut this line of code, and then go back up toward to the top of the file and write the new method. I'll add it just after beforeEach. Their structure is identical. Inside the arrow function passed to the method, I'll paste the line of code that calls verify. I now no longer need to call it at the end of the first test I wrote, so I'll go remove that line. Okay, I'm now ready to check the results of this second test. Remember that the tests are automatically running as I update the code. I'll open the console again, and we can see that 2 of 2 tests are successful. I'll leave this open, and quickly change the test code to expect the errorNumber property to be 200 instead of 100. The tests quickly rerun, and I get a failure message in the console. I'll scroll up just a little, and you can see the message Expected 100 to equal 200. I'll change the code back to 100, and the test reruns, and everything is passing again.
Writing unit tests is important if you want to deliver high-quality code that you can refactor with confidence. Thankfully, the Angular team agrees, and they built testability into the framework from the ground up, including the ability to test those all-important HTTP requests. With the help of the HttpClientTestingModule, the HttpTestingController gives you several ways to test how your code creates and processes HTTP requests and responses. I hope you'll take advantage of them, and avoid those late-night phone calls that require you to debug a problem in production. Okay, that wraps up the course. I've had fun making it, and I hope you've enjoyed watching it. And most importantly, I hope you've learned a lot along the way. Thanks for watching.
Brice has been a professional developer for over 20 years and loves to experiment with new tools and technologies. Web development and native iOS apps currently occupy most of his time.
Released9 Mar 2018