This chapter covers
Angular applications can communicate with any web server supporting HTTP, regardless of what server-side platform is used. In this chapter, we’ll show you how to use the HttpClient service offered by Angular. You’ll see how to make HTTP GET and POST methods with HttpClient. And you’ll learn how to intercept HTTP requests to implement cross-cutting concerns, such as global error handling.
This chapter starts with a brief overview of Angular’s HttpClient service, and then you’ll create a web server using the Node.js and Express.js frameworks. The server will serve the data required for most code samples in this chapter.
Finally, you’ll see how to implement HTTP interceptors and report progress while transferring large assets.
Browser-based web apps run HTTP requests asynchronously, so the UI remains responsive, and the user can continue working with the application while HTTP requests are being processed by the server. Asynchronous HTTP requests can be implemented using callbacks, promises, or observables. Although promises allow you to move away from callback hell (see section A.12.2 in appendix A), they have the following shortcomings:
Observables don’t have these shortcomings. In section 6.4 in chapter 6, we demonstrated how you can cancel HTTP requests made with observables, and in chapter 13, you’ll see how a server can push a stream of data to the client using WebSockets.
Angular supports HTTP communications via the HttpClient service from the @angular/common/http package. If your app requires HTTP communications, you need to add HttpClientModule to the imports section of the @NgModule() decorator.
If you peek inside the type definition file @angular/common/http/src/client.d.ts, you’ll see that get(), post(), put(), delete(), and many other methods return an Observable, and an app needs to subscribe to get the data. To use the HttpClient service, you need to inject it into a service or component.
As explained in chapter 5, every injectable service requires a provider declaration. The providers for HttpClient are declared in HttpClientModule, so you don’t need to declare them in your app.
The following listing illustrates one way of invoking the get() method of the HttpClient service, passing a URL as a string. You retrieve products of type Product here.
interface Product { 1 id: number, title: string } ... constructor(private httpClient: HttpClient) { } 2 ngOnInit() { this.httpClient.get<Product>('/product/123') 3 .subscribe( data => console.log(`id: ${data.id} title: ${data.title}`), 4 (err: HttpErrorResponse) => console.log(`Got error: ${err}`) 5 ); }
In the get() method, you haven’t specified the full URL (such as http://localhost:8000/product) assuming that the Angular app makes a request to the same server where it was deployed, so the base portion of the URL can be omitted. Note that in get<Product>(), you use TypeScript generics (see section B.9 in appendix B) to specify the type of data expected in the body of the HTTP response. The type annotation doesn’t enforce or validate the shape of the data returned by the server; it just makes the other code aware of the expected server response. By default, the response type is any, and the TypeScript compiler won’t be able to type-check the properties you access on the returned object.
Your subscribe() method receives and prints the data on the browser’s console. By default, HttpClient expects the data in JSON format, and the data is automatically converted into JavaScript objects. If you expect non-JSON data, use the responseType option. For example, you can read arbitrary text from a file as shown in the following listing.
let someData: string; this.httpClient .get<string>('/my_data_file.txt', {responseType: 'text'}) 1 .subscribe( data => someData = data, 2 (err: HttpErrorResponse) => console.log(`Got error: ${err}`) 3 );
The post(), put(), and delete() methods are used in a fashion similar to listing 12.2 by invoking one of these methods and subscribing to the results.
Now let’s create an app that reads some data from a JSON file.
To illustrate HttpClient.get(), your app will read a file containing JSON-formatted product data. Create a new folder that contains the products.json file shown in the following listing.
[ { "id": 0, "title": "First Product", "price": 24.99 }, { "id": 1, "title": "Second Product", "price": 64.99 }, { "id": 2, "title": "Third Product", "price": 74.99} ]
The folder data and the products.json file become assets of your app that need to be included in the project bundles, so you’ll add this folder to the app’s assets property in the .angular-cli.json file (or angular.json, starting from Angular 6), as shown in the next listing.
"assets": [ "assets", 1 "data" 2 ],
Let’s create an app that will show the product data, as shown in figure 12.1.
Your app component will use HttpClient.get() to issue an HTTP GET request, and you’ll declare a Product interface defining the structure of the expected product data. The observable returned by get() will be unwrapped in the template by the async pipe. The app.component.ts file is located in the readfile folder and has the content shown in the following listing.
interface Product { 1 id: string; title: string; price: number; } @Component({ selector: 'app-root', template: `<h1>Products</h1> <ul> <li *ngFor="let product of products$ | async"> 2 {{product.title }}: {{product.price | currency}} 3 </li> </ul> `}) export class AppComponent{ products$: Observable<Product[]>; 4 constructor(private httpClient: HttpClient) { 5 this.products$ = this.httpClient .get<Product[]>('/data/products.json'); 6 } }
In this app, you don’t use the lifecycle hook ngOnInit() for fetching data. That’s not a crime, because this code doesn’t use any component properties that may not have been initialized during component construction. This data fetch will be executed asynchronously after the constructor when the async pipe subscribes to the products$ observable.
To see this app in action, run npm install in the client directory, and then run the following command:
ng serve --app readfile -o
It wasn’t too difficult, was it? Open Chrome Dev tools, and you’ll see the HTTP request and response and their headers, as shown in figure 12.2.
This app illustrates how to make an HTTP GET request that has no parameters and uses default HTTP request headers. If you want to add additional headers and query parameters, use an overloaded version of the get() method that offers an extra parameter where you can specify additional options. The following listing shows how to request data from the same products.json file, passing additional headers and query parameters, using the classes HttpHeaders and HttpParams.
constructor(private httpClient: HttpClient) { let httpHeaders = new HttpHeaders() 1 .set('Content-Type', 'application/json') .set('Authorization', 'Basic QWxhZGRpb'); let httpParams = new HttpParams() 2 .set('title', "First"); this.products$ = this.httpClient.get<Product[]>('/data/products.json', { 3 headers: httpHeaders, params: httpParams });
Since you simply read a file, passing query parameters doesn’t make much sense, but if you needed to make a similar request to a server’s endpoint that knows how to search products by title, the code would look the same. Using the chainable set() method, you can add as many headers or query parameters as needed.
Running listing 12.7 renders the same data from products.json, but the URL of the request and HTTP headers will look different. Figure 12.3 uses arrows to highlight the differences compared to figure 12.2.
You may be wondering how to send data (for example, using HTTP POST) to the server. To write such an app, you need a server that can accept your data. In section 12.4.2, you’ll create an app that uses HttpClient.post(), but first let’s create a web server using the Node.js and Express.js frameworks.
Angular can communicate with web servers running on any platform, but we decided to create and use a Node.js server in this book for the following reasons:
You’ll start with writing a basic web server using Node and Express frameworks. Then, you’ll write another web server that can serve JSON data using the HTTP protocol. After this server’s ready, you’ll create an Angular client that can consume its data.
Source code for this chapter can be found at https://github.com/Farata/angulartypescript and www.manning.com/books/angular-development-with-typescript-second-edition.
In this section, you’ll create a web server using Node and Express (http://expressjs.com) frameworks and TypeScript. The code that comes with this chapter has a directory called server, containing a separate project with its own package .json file, which doesn’t include any Angular dependencies. The sections for dependencies and devDependencies of this file look like the following listing.
"dependencies": { "express": "^4.16.2", 1 "body-parser": "^1.18.2" 2 }, "devDependencies": { "@types/express": "^4.0.39", 3 "@types/node": "^6.0.57", 4 "typescript": "^2.6.2" 5 }
You can read about type definition files in section B.12 in appendix B. You’ll use the body-parser package for extracting the data from a request object in section 12.4.
You install the local version of the TypeScript compiler just in case you need to keep a different version of the tsc compiler installed globally. Also, you shouldn’t expect that a continuous integration server has a global tsc executable. To use the local tsc version, you can add a custom npm script command in the scripts section of package.json ("tsc": "tsc") and start the compiler by running the npm run tsc command.
Because you’ll write the server code in TypeScript, it needs to be transpiled, so the following listing adds the tsconfig.json file with the compiler’s options for tsc.
{ "compilerOptions": { "module": "commonjs", 1 "outDir": "build", 2 "target": "es6" 3 }, "exclude": [ "node_modules" 4 ] }
By specifying the CommonJS syntax for modules, you ensure that tsc transpiles statements like import * as express from "express"; into const express = require ("express");, as required by Node.
The following listing shows the code of a simple web server from the file my-express-server.ts. This server implements the server-side routing for HTTP GET requests by mapping three paths—/, /products, and /reviews—to the corresponding callback functions.
import * as express from "express"; const app = express(); 1 app.get('/', (req, res) => res.send('Hello from Express')); 2 app.get('/products', (req, res) => res.send('Got a request for products')); 3 app.get('/reviews', (req, res) => res.send('Got a request for reviews')); 4 const server = app.listen(8000, "localhost", () => { 5 console.log(`Listening on localhost:8000`); });
Run npm install; transpile all code samples, including my-express-server.ts, by running tsc; and start this server by running the following command:
node build/my-express-server
If you don’t have the TypeScript compiler installed globally, you can either run its local version, ./node_modules/typescript/bin/tsc, or add the line "tsc: "tsc" to the scripts section of package.json, and run it like this: npm run tsc.
You’ll see the message “Listening on localhost:8000” on the console, and now you can request either products or reviews, depending on which URL you enter in the browser, as shown in figure 12.4.
To debug Node applications, refer to the documentation of your IDE. If you want to debug the TypeScript code, don’t forget to set the option "sourceMap": true in the tsconfig.json file of your Node project.
This server responds with simple text messages, but how do you create a server that can respond with data in JSON format?
To send JavaScript objects (such as products) to the browser in JSON format, you’ll use the Express function json() on the response object. Your REST server is located in the rest-server.ts file, and it can serve either all products or a specific one (by ID). In this server, you’ll create three endpoints: / for the root path, /api/products for all products, and /api/products/:id for the paths that include product IDs. The products array will contain three hardcoded objects of type Product, which will be turned into JSON format by invoking res.json(), offered by the Express framework.
import * as express from "express"; const app = express(); interface Product { 1 id: number, title: string, price: number } const products: Product[] = [ 2 {id: 0, title: "First Product", price: 24.99}, {id: 1, title: "Second Product", price: 64.99}, {id: 2, title: "Third Product", price: 74.99} ]; function getProducts(): Product[] { 3 return products; } app.get('/', (req, res) => { 4 res.send('The URL for products is http://localhost:8000/api/products'); }); app.get('/api/products', (req, res) => { 5 res.json(getProducts()); 6 }); function getProductById(productId: number): Product { 7 return products.find(p => p.id === productId); } app.get('/api/products/:id', (req, res) => { 8 res.json(getProductById(parseInt(req.params.id))); 9 }); const server = app.listen(8000, "localhost", () => { console.log(`Listening on localhost:8000`); });
Stop the my-express-server from the previous section if it’s running (Ctrl-C), and start the rest-server with the following command:
node build/rest-server
Enter http://localhost:8000/api/products in the browser, and you should see the data in JSON format, as shown in figure 12.5.
Figure 12.6 shows the browser window after you enter the URL http://localhost:8000/api/products/1. This time, the server returns only data about the product that has an id with the value of 1.
Your REST server is ready. Now let’s see how to initiate HTTP GET requests and handle responses in Angular applications.
In the preceding section, you created the rest-server.ts file, which responds to HTTP GET requests with product details regardless of whether the client was written using a framework or the user simply entered the URL in a browser. In this section, you’ll write an Angular client that will issue HTTP GET requests and treat the product data as an Observable data stream returned by your server.
Just a reminder: the Angular app and the Node server are two separate projects. The server code is located in the directory called server, and the Angular app is located in a separate project in the client directory.
A typical web app deployed on the server includes static assets (for example, HTML, images, CSS, and JavaScript code) that have to be loaded by the browser when the user enters the URL of the app. From the server’s perspective, the Angular portion of a web app is considered static assets. The Express framework allows you to specify the directory where the static assets are located.
Let’s create a new server: rest-server-angular.ts. In the rest-server.ts file from the previous section, you didn’t specify the directory with static assets, because no client app was deployed on the server. In the new server, you add the lines shown in the following listing.
import * as path from "path"; 1 app.use('/', express.static(path.join(__dirname, 'public'))); 2
Unlike in rest-server.ts, you just map the base URL (/) to the public directory, and Node will send index.html from there by default. The browser loads index.html, which in turn loads the rest of the bundles defined in the <script> tags.
The original index.html file generated by Angular CLI doesn’t contain <script> tags, but when you run the ng build or ng serve commands, they create a new version of index.html that includes the <script> tags with all the bundles and other assets.
When the browser requests static assets, Node will look for them in the public subdirectory of the current one (__dirname)—the build directory from which you started this server. Here, you use Node’s path.join() API to ensure that the absolute file path is created in a cross-platform way. In the next section, we’ll introduce the Angular client and deploy its bundles in the public directory. The REST endpoints in rest-server-angular.ts remain the same as in rest-server.ts:
The complete code of the rest-server-angular.ts file is shown in the next listing.
import * as express from "express"; import * as path from "path"; 1 const app = express(); app.use('/', express.static(path.join(__dirname, 'public'))); 2 interface Product { id: number, title: string, price: number } const products: Product[] = [ {id: 0, title: "First Product", price: 24.99}, {id: 1, title: "Second Product", price: 64.99}, {id: 2, title: "Third Product", price: 74.99} ]; function getProducts(): Product[] { return products; } app.get('/api/products', (req, res) => { 3 res.json(getProducts()); }); function getProductById(productId: number): Product { return products.find(p => p.id === productId); } app.get('/api/products/:id', (req, res) => { 4 res.json(getProductById(parseInt(req.params.id))); }); const server = app.listen(8000, "localhost", () => { 5 console.log(`Listening on localhost:8000`); });
The new server is ready to serve JSON data to the Angular client, so let’s start it:
node build/rest-server-angular
Trying to make a request to this server using the base URL http://localhost:8000 will return a 404 error, because the directory with static assets doesn’t contain the index.html file: you haven’t deployed your Angular app there yet. Your next task is to create and deploy the Angular app that will consume JSON-formatted data.
Your Angular app will be located in a directory called client. In previous chapters, you were starting all Angular apps building bundles in memory with ng serve, but this time you’ll also use the ng build command to generate bundles in files. Then you’ll use npm scripts to automate the deployment of these bundles in the Node server created in section 12.4.1.
In dev mode, you’ll keep serving Angular apps using the dev server from Angular CLI that runs on port 4200. But the data will be coming from another web server, powered by Node and Express, that will run on port 8000. Figure 12.7 illustrates this two-server setup.
Spoiler alert: We’ll run into an issue when the client app served from one server tries to directly access another one. We’ll cross that bridge when we get to it.
When the Angular HttpClient object makes a request to a URL, the response comes back as Observable, and the client’s code can handle it either by using the subscribe() method or with the async pipe introduced in section 6.5 in chapter 6. Using the async pipe is preferable, but we’ll show you both methods so you can appreciate the advantages of async.
Let’s start with an app that retrieves all products from the rest-server-angular server and renders them using an HTML unordered list (<ul>). You can find this app in the app.component.ts file located in the client/src/app/restclient directory.
// import statements omitted for brevity interface Product { 1 id: number, title: string, price: number } @Component({ selector: 'app-root', template: `<h1>All Products</h1> <ul> <li *ngFor="let product of products"> {{product.title}}: {{product.price | currency}} 2 </li> </ul> {{error}} `}) export class AppComponent implements OnInit { products: Product[] = []; theDataSource$: Observable<Product[]>; 3 productSubscription: Subscription; 4 error: string; 5 constructor(private httpClient: HttpClient) { 6 this.theDataSource$ = this.httpClient .get<Product[]>('http://localhost:8000/api/products'); 7 } ngOnInit() { this.productSubscription = this.theDataSource$ .subscribe( 8 data => this.products = data, 9 (err: HttpErrorResponse) => this.error = `Can't get products. Got ${err.message}` 10 ); } }
You didn’t use ngOnDestroy() to explicitly unsubscribe from the observable because once HttpClient gets the response (or an error), the underlying Observable completes, so the observer is unsubscribed automatically.
You already started the server in the previous section. Now, start the client by running the following command:
ng serve --app restclient -o
No products are rendered by the browser, and the console shows a 404 error, but if you used the full URL in the AppComponent (for example, http://localhost:8000/api/products), the browser’s console would show the following error:
Failed to load http://localhost:8000/api/products: No 'Access-Control-Allow- Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access.
That’s because you violated the same-origin policy (see http://mng.bz/2tSb). This restriction is set for clients that run in a browser as a security mechanism. Say you visited and logged in to bank.com, and then opened another tab and opened badguys.com. The same-origin policy ensures that scripts from badguys.com can’t access your account at bank.com.
Your Angular app was loaded from http://localhost:4200 but tries to access the URL http://localhost:8000. Browsers aren’t allowed to do this unless the server that runs on port 8000 is configured to allow access to the clients with the origin http://localhost:4200. When your client app is deployed in the Node server, you won’t have this error, because the client app will be loaded from the server that runs on port 8000, and this client will be making data requests to the same server.
In the hands-on section in chapter 13, you’ll use the Node.js CORS package (see https://github.com/expressjs/cors) to allow requests from clients with other origins, but this may not be an option if you need to make requests to third-party servers. In dev mode, there’s a simpler solution to the same-origin restriction. You’ll use the server that runs on port 4200 as a proxy for client requests to the server that runs on port 8000. The same-origin policy doesn’t apply to server-to-server communications. In the next section, you’ll see how to configure such a proxy on the client.
In dev mode, you’d like to continue using the server that comes with Angular CLI with its hot reload features and fast rebuilding of application bundles in memory. On the other hand, you want to be able to make requests to other servers.
Under the hood, the Angular CLI dev server uses the Webpack dev server, which can serve as a proxy mediating browser communications with other servers. You just need to create a proxy-conf.json file in the root directory of the Angular project, where you’d configure a URL fragment(s) that the dev server should redirect to another server. In your case, you want to redirect any request that has the URL fragment /api to the server that runs on port 8000, as shown in the following listing.
{ "/api": { 1 "target": "http://localhost:8000", 2 "secure": false 3 } }
Using a proxy file allows you to easily switch between local and remote servers. Just change the value of the target property to have your local app retrieve data from a remote server. You can read more about Angular CLI proxying support at http://mng.bz/fLgf.
You need to make a small change in the app from the preceding section. You should replace the full URL of the backend server (http://localhost:8000/api/products) with the path of the endpoint (/api/products). The code that makes a request for products will look as if you try to access the /api/products endpoint on the Angular CLI dev server where the app was downloaded from:
this.theDataSource = this.httpClient .get<Product[]>('/api/products');
But the dev server will recognize the /api fragment in the URL and will redirect this request to another server that runs on port 8000, as shown in figure 12.8 (compare it to figure 12.7).
To see the modified app in action, you need to use the --proxy-config option, providing the name of the file where you configured the proxy parameters:
ng serve --app restclient --proxy-config proxy-conf.json -o
If you forget to provide the name of the proxy file when configuring proxy parameters, you’ll get a 404 error, because the /api/products request won’t be redirected, and there’s no such endpoint in the server that runs on port 4200.
Open your browser to http://localhost:4200, and you’ll see the Angular app shown in figure 12.9.
Note that data arrives from the server that runs on port 4200, which got it from the server that runs on port 8000. Figure 12.8 illustrates this data flow.
In dev mode, using Angular CLI proxying allows you to kill two birds with one stone: have the hot reload of your app on any code change and access data from another server without the need to deploy the app there.
Now let’s see how to replace the explicit subscription for products with the async pipe.
We introduced AsyncPipe (or async, when used in templates) in section 6.5 of chapter 6. async can receive an Observable as input, autosubscribe to it, and discard the subscription when the component gets destroyed. To see this in action, make the following changes in listing 12.13:
The following listing implements these changes (see the file restclient/app.component.asyncpipe.ts).
import { HttpClient} from '@angular/common/http'; import {Observable, EMPTY} from 'rxjs'; import {catchError} from 'rxjs/operators'; import {Component} from "@angular/core"; interface Product { id: number, title: string, price: number } @Component({ selector: 'app-root', template: `<h1>All Products</h1> <ul> <li *ngFor="let product of products$ | async"> 1 {{product.title }} {{product.price | currency}} </li> </ul> {{error}} `}) export class AppComponentAsync{ products$: Observable<Product[]>; error: string; constructor(private httpClient: HttpClient) { this.products$ = this.httpClient.get<Product[]>('/api/products') 2 .pipe( catchError( err => { 3 this.error = `Can't get products. Got ${err.status} from ${err.url}`;4 return EMPTY; }); 5 } }
Running this application will produce the same output shown in figure 12.9.
So far, you’ve been injecting HttpClient instances directly into components, but more often you inject HttpClient into a service. Let’s see how to do this.
Angular offers an easy way for separating the business logic implementation from rendering the UI. Business logic should be implemented in services, and the UI in components, and you usually implement all HTTP communications in one or more services that are injected into components. For example, your ngAuction app that comes with chapter 11 has the ProductService class with the injected HttpClient service. You inject a service into another service.
ProductService reads the products.json file using HttpClient, but it could get the product data from a remote server the same way you did in the previous section. ProductService is injected into components of ngAuction. Check the source code of ProductService and CategoriesComponent in ngAuction that comes with chapter 11, and you’ll recognize the pattern shown in figure 12.10.
The following listing from ngAuction’s ProductService is an example of encapsulating business logic and HTTP communications inside a service.
@Injectable() export class ProductService { constructor(private http: HttpClient) {} 1 getAll(): Observable<Product[]> { return this.http.get<Product[]>('/data/products.json'); 2 } getById(productId: number): Observable<Product> { return this.http.get<Product[]>('/data/products.json') 2 .pipe( map(products => products.find(p => p.id === productId)) ); } getByCategory(category: string): Observable<Product[]> { return this.http.get<Product[]>('/data/products.json').pipe( 2 map(products => products.filter(p => p.categories.includes(category))) ); } getDistinctCategories(): Observable<string[]> { return this.http.get<Product[]>('/data/products.json') 2 .pipe( map(this.reduceCategories), map(categories => Array.from(new Set(categories))), ); } // Other code is omitted for brevity }
In the preceding listing 12.19, you use RxJS pipeable operators inside the pipe() method (see section D.4.1 in appendix D).
The next listing from CategoriesComponent is an example of using the preceding service in the component.
@Component({ selector: 'nga-categories', styleUrls: [ './categories.component.scss' ], templateUrl: './categories.component.html' }) export class CategoriesComponent { readonly categoriesNames$: Observable<string[]>; readonly products$: Observable<Product[]>; constructor( private productService: ProductService, 1 private route: ActivatedRoute ) { this.categoriesNames$ = this.productService.getDistinctCategories() 2 .pipe(map(categories => ['all', ...categories])); this.products$ = this.route.params.pipe( switchMap(({ category }) => this.getCategory(category))); } private getCategory(category: string): Observable<Product[]> { return category.toLowerCase() === 'all' ? this.productService.getAll() 2 : this.productService.getByCategory(category.toLowerCase()); 2 } }
The provider for ProductService is declared on the app level in the @NgModule() decorator of the root module of ngAuction. In the hands-on section in chapter 13, you’ll split ngAuction into two projects, client and server, and the web server will be written using Node and Express frameworks. How can an Angular app (bundles and assets) be deployed in a web server?
The process of deploying the code of a web client in a server should be automated. At the very minimum, deploying an Angular app includes running several commands for building bundles and replacing previously deployed code (index.html, JavaScript bundles, and other assets) with new code. The deployment may also include running test scripts and other steps.
JavaScript developers use various tools for automating running deployment tasks such as Grunt, gulp, npm scripts, and others. In this section we’ll show you how to use npm scripts for deployment. We like using npm scripts, because they’re simple to use and offer an easy way to automate running command sequences in a predefined order. Besides, you already have npm installed, so there’s no need to install additional software for automating your deployment workflow.
To illustrate the deployment process, you’ll use the rest-server-angular server from section 12.3.1, where you’ll deploy the Angular app from section 12.3.4. After deployment, you won’t need to configure the proxy anymore, because both the server and the client code will be deployed at the same server running http://localhost:8000. After entering this URL in the browser, the user will see the product data, as shown earlier in figure 12.9.
npm allows you to add the scripts property in package.json, where you can define aliases for terminal commands. For example, instead of typing the long command ng serve --app restclient --proxy-config proxy-conf.json, you can define a start command in the scripts section of package.json as follows:
"scripts": { "start": "ng serve --app restclient --proxy-config proxy-conf.json" }
Now, instead of typing that long command, you’ll just enter npm start in the console. npm supports more than a dozen script commands right out of the box (see the npm-scripts documentation for details, https://docs.npmjs.com/misc/scripts). You can also add new custom commands specific to your development and deployment workflow.
Some of these scripts need to be run manually (such as npm start), and some are invoked automatically if they have the post and pre prefixes (for example, post-install). If any command in the scripts section starts with the post prefix, it’ll run automatically after the corresponding command specified after this prefix.
For example, if you define the command "postinstall": "myCustomInstall.js", each time you run npm install, the script myCustomInstall.js will automatically run right after. Similarly, if a command has a pre prefix, such a command will run before the command named after this prefix.
If you define custom commands that aren’t known by npm scripts, you’ll need to use an additional option: run. Say you defined a custom command startDev like this:
"scripts": { "startDev": "ng serve --app restclient --proxy-config proxy-conf.json" }
To run that command, you need to enter the following in your terminal window: npm run startDev. To automate running some of your custom commands, use the same prefixes: post and pre.
Let’s see how to create a sequence of runnable commands for deploying an Angular app on the Node server. Open package.json from the client directory, and you’ll find four custom commands there: build, postbuild, predeploy, and deploy. The following listing shows what will happen if you run a single command: npm run build.
"scripts": { "build": "ng build --prod --app restclient", 1 "postbuild": "npm run deploy", 2 "predeploy": "rimraf ../server/build/public && mkdirp ../server/build/public",3 "deploy": "copyfiles -f dist/** ../server/build/public" 4 }
We’ll explain what the commands predeploy and deploy do in a minute, but our main message here is that starting a single command resulted in running four commands in the specified order. Creating a sequence of deployment commands is easy.
If you build the bundles with AOT compilation and use only standard Angular decorators (no custom ones), you can further optimize the size of the JavaScript in your app by commenting out the line import 'core-js/es7/ reflect'; in the polyfills.ts file. This will reduce the size of the generated polyfill bundle.
Typically, the deployment process removes the directory with previously deployed files, creates a new empty directory, and copies the new files into this directory. In your deployment scripts, you use three npm packages that know how to do these operations, regardless of the platform you use (Windows, Unix, or macOS):
Check the devDependencies section in package.json, and you’ll see rimraf, mkdirp, and copyfiles there.
Currently, Angular CLI uses Webpack to build bundles. Angular CLI 7 will come with new build tools. In particular, it’ll include Closure Compiler, which produces smaller bundles.
The code that comes with this chapter is located in two sibling directories: client and server. Your predeploy command removes the content of the server/build/public directory (this is where you’ll deploy the Angular app) and then creates a new empty public directory. The && sign allows you to define commands that run more than one script.
The deploy command copies the content of the client/dist directory (the app’s bundles and assets) into server/build/public.
In the real world, you may need to deploy an Angular app on a remote server, so using the package copyfiles won’t work. Consider using an SCP utility (see https://en.wikipedia.org/wiki/Secure_copy) that performs secure file transfer from a local computer to a remote one.
If you can manually run a utility from the terminal window, you can run it using npm scripts as well. In chapter 14, you’ll learn how to write test scripts. Including a test runner into your build process could be as simple as adding && ng test to your predeploy command. If you find some useful gulp plugins, create the npm script for it, for example, "myGulpCommand" : "gulp SomeUsefulTask".
To see that your deployment scripts work, perform the following steps:
1. Start the server by running the following command in the server directory:
node build/rest-server-angular
2. In the client directory, run the build and deployment scripts:
Check the server/build/public directory—the client’s bundles should be there.
3. Open your browser to http://localhost:8000, and your Angular app will be loaded from your Node server showing three products, as shown in figure 12.9.
We’ve described the entire process of creating and running a web server, as well as creating and running Angular apps in dev mode and deploying in the server. Your Angular app was using the HttpClient service to issue HTTP GET requests to retrieve data from the server. Now let’s see how to issue HTTP POST requests to post data to the server.
HTTP POST requests are used for sending new data to the server. With HttpClient, making POST requests is similar to making GET requests. Invoking the HttpClient.post() method declares your intention to post data to the specified URL, but the request is made when you invoke subscribe().
For updating existing data on the server, use HttpClient.put(); and for deleting data, use HttpClient.delete().
You need a web server with an endpoint that knows how to handle POST requests issued by the client. The code that comes with this chapter includes a server with an /api/product endpoint for adding new products, located in the rest-server-angular-post.ts file. Because your goal isn’t to have a fully functional server for adding and saving products, the /api/product endpoint will simply log the posted data on the console and send a confirmation message to the client.
The posted data will arrive in the request body, and you need to be able to parse it to extract the data. The npm package body-parser knows how to do this in Express servers. If you open package.json in the server directory, you’ll find body-parser in the dependencies section. The entire code of your server is shown in the following listing.
import * as express from "express"; import * as path from "path"; import * as bodyParser from "body-parser"; 1 const app = express(); app.use('/', express.static(path.join(__dirname, 'public'))); app.use(bodyParser.json()); 2 app.post("/api/product", (req, res) => { 3 console.log(`Received new product ${req.body.title} ${req.body.price}`); 4 res.json({'message':`Server responded: added ${req.body.title}`}); 5 }); const server = app.listen(8000, "localhost", () => { const {address, port} = server.address(); console.log(`Listening on ${address}: ${port}`); });
Your server expects the payload in a JSON format, and it’ll send the response back as a JSON object with one property: message. Start this server by running the following command in the server directory (don’t forget to run tsc to compile it):
node build/rest-server-angular-post
When you create web servers with REST endpoints, you should test them to ensure that the endpoints work properly even before you start writing any client code. Your IDE may offer such a tool. For example, the WebStorm IDE has a menu item, Test RESTful Web Service, under Tools. After entering all the data to test your server, this tool looks like the following figure.
Using WebStorm’s Test RESTful client
The server response
Press the green play button, and you’ll see the response of your /api/product endpoint under the Response tab, as shown in the next figure.
The server response
If your IDE doesn’t offer such a testing tool, use the Chrome extension called Advanced REST Client (https://install.advancedrestclient.com/#/install) or a tool called Postman (www.getpostman.com).
Now that you’ve created, started, and tested the web server, let’s write the Angular client that will post new products to this server.
Your Angular app will render a simple form where the user can enter the product title and price, as shown in figure 12.11.
After filling out the form and clicking the Add Product button, the server will respond with the confirmation message shown under the button.
In this app, you’ll use the template-driven Forms API, and your form will require the user to enter the new product’s title and price. On the button click, you’ll invoke the method HttpClient.post(), followed by subscribe().
The code of this Angular app is located in the client directory under the restclientpost subdirectory. It’s shown in the following listing.
import {Component} from "@angular/core"; import {HttpClient, HttpErrorResponse} from "@angular/common/http"; @Component({ selector: 'app-root', template: `<h1>Add new product</h1> <form #f="ngForm" (ngSubmit) = "addProduct(f.value)" > Title: <input id="productTitle" name="title" ngModel> <br> Price: <input id="productPrice" name="price" ngModel> <br> <button type="submit">Add product</button> </form> {{response}} `}) export class AppComponent { response: string; constructor(private httpClient: HttpClient) {} addProduct(formValue) { this.response=''; this.httpClient.post<string>("/api/product", 1 formValue) 2 .subscribe( 3 data => this.response = data['message'], 4 (err: HttpErrorResponse) => 5 this.response = `Can't add product. Error code: ${err.message} ${err.error.message}` ); } }
When the user clicks Add Product, the app makes a POST request and subscribes to the server response. formValue contains a JavaScript object with the data entered in the form, and HttpClient automatically turns it into a JSON object. If the data was posted successfully, the server returns a JSON object with the message property, which is rendered using data binding.
If the server responds with an error, you display it in the UI. Note that you use err.message and err.error.message to extract the error description. The second property may contain additional error details. Modify the code of the server to return a string instead of JSON, and the UI will show a detailed error message.
To see this app in action, run the following command in the project client:
ng serve --app restclientpost --proxy-config proxy-conf.json -o
You now know how to send HTTP requests and handle responses, and there could be lots of them in your app. Is there a way to intercept all of them to provide some additional processing, like showing/hiding the progress bar, or logging requests?
Angular allows you to create HTTP interceptors for pre- and post-processing of all HTTP requests and responses of your app. They can be useful for implementing such cross-cutting concerns as logging, global error handling, authentication, and others. We’d like to stress that the interceptors work before the request goes out or before a response is rendered on the UI. This gives you a chance to implement the fallback scenarios for certain errors or prevent attempts of unauthorized access.
To create an interceptor, you need to write a service that implements the HttpInterceptor interface, which requires you to implement one method: intercept(). Angular will provide two arguments to this callback: HttpRequest and HttpHandler. The first one contains the request object being intercepted, which you can clone and modify. The second argument is used to forward the modified request to the backend or another interceptor in the chain (if any) by invoking the handle() method.
The HttpRequest and HttpResponse objects are immutable, and the word modify means creating and passing through the new instances of these objects.
The interceptor service shown in the following listing doesn’t modify the outgoing HttpRequest but simply prints its content on the console and passes it through as is.
@Injectable() export class MyFirstInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // Clone and modify your HTTPRequest using req.clone() // or perform other actions here console.log("I've intercepted your HTTP request! ${JSON.stringify(req)}`); return next.handle(req); } }
In listing 12.21, you forward the HttpRequest, but you could modify its headers or parameters and return the modified request. The next.handle() method returns an observable when the request is complete, and if you want to modify the HTTP response as well, apply additional RxJS operators on the stream returned by next.handle().
The intercept() method receives the HttpRequest object and returns, not the HttpResponse object, but the observable of HttpEvent, because Angular implements HttpResponse as a stream of HttpEvent values.
Both HttpRequest and HttpResponse are immutable, and if you want to modify their properties, you need to clone them first, as in the following listing.
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any >> { const modifiedRequest = req.clone({ setHeaders: { ('Authorization', 'Basic QWxhZGRpb') } }); return next.handle(modifiedRequest); }
Because an interceptor is an injectable service, don’t forget to declare its provider for the HTTP_INTERCEPTORS token in the @NgModule() decorator:
providers: [{provide: HTTP_INTERCEPTORS, useClass: MyFirstInterceptor, multi: true}]
The multi: true option tells you that HTTP_INTERCEPTORS is a multiprovider token—an array of services can represent the same token. You can register more than one interceptor, and Angular will inject all of them:
providers: [{provide: HTTP_INTERCEPTORS, useClass: MyFirstInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: MySecondInterceptor, multi: true}]
If you have more than one interceptor, they’ll be invoked in the order they’re defined.
To illustrate how interceptors work, let’s create an app with an HttpInterceptor that will intercept and log all errors returned by the server. For the client, you’ll reuse the app from section 12.5.2 shown in figure 12.11, adding the logging service and the interceptor to log errors on the console.
You’ll slightly modify the server from the previous section to randomly generate errors. You can find the complete code of the server in the rest-server-angular-post-errors.ts file. Now, instead of just responding with success messages, it’ll randomly return an error, as shown in the following listing.
if (Math.random() < 0.5) { 1 res.status(500); res.send({'message': `Server responded: error adding product ${req.body.title}`}); } else { 2 res.send({'message': `Server responded: added ${req.body.title}`); }
Start this server as follows:
node build/rest-server-angular-post-errors
Your Angular app is located in the interceptor directory and includes a logging service implemented as two classes: LoggingService and ConsoleLoggingService. LoggingService is an abstract class that declares one method, log().
@Injectable() export abstract class LoggingService { abstract log(message: string): void; }
Because this class is abstract, it can’t be instantiated, and you’ll create the class ConsoleLoggingService shown in the following listing.
@Injectable() export class ConsoleLoggingService implements LoggingService{ log(message:string): void { console.log(message); } }
You may be wondering why you create the abstract class for such a simple logging service. It’s because in real-world apps, you may want to introduce logging, not only on the browser’s console, but also on the server. Having an abstract class would allow you to use it as a token for declaring a provider:
providers: [{provide: LoggingService, useClass: ConsoleLoggingService}]
Later on, you can create a class called ServerLoggingService that implements LoggingService, and to switch from console to server logging, you’ll need to change the provider without having to modify components that use it:
providers: [{provide: LoggingService, useClass: ServerLoggingService}]
If your interceptor receives an error, you’ll do the following:
1. Log it on the console.
2. Replace the HttpErrorResponse with a new instance of HttpResponse that will contain the error message.
3. Return the new HttpResponse so the client can show it to the user.
The interceptor class will use the catchError operator on the observable returned by HttpHandler.next(), where you’ll implement these steps. Your interceptor is implemented in the logging.interceptor.service.ts file.
import {Injectable} from "@angular/core"; import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from "@angular/common/http"; import {Observable, of} from "rxjs"; import {catchError} from 'rxjs/operators'; import {LoggingService} from "./logging.service"; @Injectable() export class LoggingInterceptor implements HttpInterceptor { constructor(private loggingService: LoggingService) {} 1 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req) 2 .pipe( catchError((err: HttpErrorResponse) => 3 this.loggingService.log(`Logging Interceptor: ${err.error.message}`);4 return of(new HttpResponse( 5 {body:{message: err.error.message}})); 6 }) ); } }
The code for the application component has no references to the interceptor class, as you’ll see in the following listing. It’ll be always receiving HttpResponse objects that contain either a message that the server successfully added a new product, or an error message.
import {Component} from "@angular/core"; import {HttpClient} from "@angular/common/http"; import {Observable} from "rxjs"; import {map} from "rxjs/operators"; @Component({ selector: 'app-root', template: `<h1>Add new product</h1> <form #f="ngForm" (ngSubmit) = "addProduct(f.value)" > Title: <input id="productTitle" name="title" ngModel> <br> Price: <input id="productPrice" name="price" ngModel> <br> <button type="submit">Add product</button> </form> {{response$ | async}} 1 `}) export class AppComponent { response$: Observable<string>; 2 constructor(private httpClient: HttpClient) {} addProduct(formValue){ this.response$=this.httpClient.post<{message: string}>("/api/product", 3 formValue) .pipe( map (data=> data.message) 4 ); } }
When you compare this app with the one from the previous section, note that you don’t handle errors in the component, which renders the messages to the UI. Now the LoggingInterceptor will handle all HTTP errors.
To see this app in action, run the following command and monitor the browser console for logging messages.
ng serve --app interceptor --proxy-config proxy-conf.json -o
This app should give you an idea of how to implement a cross-cutting concern like a global error-logging service for all HTTP responses without the need to modify any application components or services that use the HttpClient service.
An HTTP request runs asynchronously and can generate a number of progress events that you might want to intercept and handle. Let’s look at how you’d do that.
Sometimes uploading or downloading certain assets (like large data files or images) takes time, and you should keep the user informed about the progress. HttpClient offers progress events that contain information like total size of the asset, current number of bytes that are already uploaded or downloaded, and more.
To enable progress events tracking, make your requests using the HttpRequest object with the option {reportProgress: true}. For example, you can make an HTTP GET request that reads the my_large_file.json file.
const req = new HttpRequest('GET', 1 './my_large_file.json', 2 { reportProgress: true }); 3 httpClient.request(req).subscribe( 4 // Handle progress events here);
In the subscribe() method, check whether the emitted value is an event of the type you’re interested in, for example, HttpEventType.DownloadProgress or HttpEventType.UploadProgress. These events have the loaded property for the current number of transferred bytes and the total property, which knows the total size of the transfer.
The next app shows how to handle a progress event for calculating and showing the percentage of the file download. This app comes with a large 48 MB JSON file. The content of the file is irrelevant in this case. Figure 12.12 shows the app when the download of the file is complete. The percentage on the left is changing as this file is being loaded by HttpClient. This app also reports the progress on the browser’s console.
This app is located in the progressevents directory, and the content of app.component .ts is shown in the next listing.
import {HttpClient, HttpEventType, HttpRequest} from '@angular/common/http'; import {Component} from "@angular/core"; @Component({ selector: 'app-root', template: `<h1>Reading a file: {{percentDone}}% done</h1> 1 `}) export class AppComponent{ mydata: any; percentDone: number; constructor(private httpClient: HttpClient) { const req = new HttpRequest('GET', 2 './data/48MB_DATA.json', 3 {reportProgress: true}); 4 httpClient.request(req) .subscribe(data => { if (data.type === HttpEventType.DownloadProgress) { 5 this.percentDone = Math.round(100 * data.loaded / data.total);)) 6 console.log(`Read ${this.percentDone}% of ${data.total} bytes`); } else { this.mydata = data 7 } }); } }
To see this app in action, run the following command:
ng serve --app progressevents -o
This app concludes our coverage of communicating with web servers using HTTP. In the next chapter, you’ll see how an Angular client can communicate with web servers using WebSockets.
18.219.14.63