Jerry's Blog

Recording what I learned everyday

View on GitHub


2 July 2019

Angular (19) -- HTTP requests

by Jerry Zhang

Day 26: Angular HTTP requests

Sending a simple POST Request

constructor(private http: HttpClient) {}
onCreatePost(postData: { title: string; content: string }) {
    // Send Http request
    this.http
      .post(
        'https://recipebook-cc2b1.firebaseio.com/posts.json',
        postData
      )
      .subscribe(responseData => {
        console.log(responseData);
      });
  }

The URL above ends with .json only because we are using Firebase. Usually, we do not add this extension here.

Angular will convert JavaScript object to JSON format for us automatically.

IMPORTANT: Angular uses Observables for Http requests. So we must subscribe the requests, otherwise Angular does not send the requests at all!

HTTP Requests will return an Observable

Getting Data

private fetchPosts() {
    this.http.get('https://recipebook-cc2b1.firebaseio.com/posts.json')
      .subscribe(posts => {
        console.log(posts);
      });
  }

Using RXJS Operator to Transform Response Data

Before we subscribe, we can call the pipe method to do something first. For example, we want to add a filter, or pre-process the data first.

Here, we need the map operator. The map operator allows us to get some data and return new data which is then automatically re-wrapped into an Observable, so that we can still subscribe it.

private fetchPosts() {
    this.http.get('https://recipebook-cc2b1.firebaseio.com/posts.json')
      .pipe(map(responseData => {
        const postsArray = [];
        for (const key in responseData) {
          if (responseData.hasOwnProperty(key)) {
            postsArray.push({ ...responseData[key], id: key});
          }
        }
        return postsArray;
      }))
      .subscribe(posts => {
        console.log(posts);
      });
  }

Loop an object in JavaScript

In the code above, we first use a for...in... loop to fetch each object in the array.

In ES6, we have better ways to do this.

const fruits = {
  apple: 28,
  orange: 17,
  pear: 54,
}

const keys = Object.keys(fruits)
console.log(keys) // [apple, orange, pear]

Three dots operator

Three dots basically means “does not know how many” or “no matter how many”. For example,

function myFunc(a, b, ...args) {
 console.log(a); // 22
 console.log(b); // 98
 console.log(args); // [43, 3, 26]
};
myFunc(22, 98, 43, 3, 26);

In our case, we do not know how many key-value pairs in one object, but we want to push all of them into our array as a single object together with the id.

Do not forget to return this array from the map method.

Using Types with the HttpClient

We can tell typescript our responseData type.

Create a model:

export interface Post {
  title: string;
  content: string;
  id?: string;
}

The id is optional.

Then we define can rewrite our code:

private fetchPosts() {
    this.http
      .get<{[key: string]: Post }>('https://recipebook-cc2b1.firebaseio.com/posts.json')
      .pipe(map(responseData => {
        const postsArray: Post[] = [];
        for (const key in responseData) {
          if (responseData.hasOwnProperty(key)) {
            postsArray.push({ ...responseData[key], id: key});
          }
        }
        return postsArray;
      }))
      .subscribe(posts => {
        console.log(posts);
      });
  }

We can declare the response body after the get as a generic type. This way is recommended.

Outputting Posts

After we get the response data, we can save it into an attribute. Then we can display it in our template.

.subscribe(
      posts => {
this.loadedPosts = posts;
});
<p *ngIf="loadedPosts.length < 1">No posts available!</p>
<ul class="list-group" *ngIf="loadedPosts.length >= 1">
<li class="list-group-item" *ngFor="let post of loadedPosts">
  <h3></h3>
  <p></p>
</li>
</ul>

Very useful feature: Showing a loading indicator

Create a new attribute isFetching. Then, each time before we sending a http request, we set it to true. After we complete, we set it back to false.

To display it:

<p *ngIf="loadedPosts.length < 1 && !isFetching">No posts available!</p>
      <ul class="list-group" *ngIf="loadedPosts.length >= 1 && !isFetching">
        <li class="list-group-item" *ngFor="let post of loadedPosts">
          <h3></h3>
          <p></p>
        </li>
      </ul>
      <p *ngIf="isFetching">Loading...</p>

Handling Errors

We can create a new property called error.

error = null;

The second parameter of our subscribe method is the error callback function.

onFetchPosts() {
    // Send Http request
    this.isFetching = true;
    this.postsService.fetchPosts().subscribe(
      posts => {
      this.isFetching = false;
      this.loadedPosts = posts;
    }, error => {
      this.error = error.message;
    });
  }

We can display the error.

<p *ngIf="isFetching && !error">Loading...</p>
  <div class="alert alert-danger" *ngIf="error">
    <h1>An Error Occurred!</h1>
    <p></p>
  </div>

Handling an Error with Subjects

If we subscribe the Observable in the service, then we cannot get the error message directly. So we can use Subjects. It is also useful if multiply places are interested in the error.

error = new Subject<string>();

createAndStorePost(title: string, content: string) {
    const postData: Post = {title, content};
    this.http
      .post<{ name: string }>(
        'https://recipebook-cc2b1.firebaseio.com/posts.json',
        postData
      )
      .subscribe(responseData => {
        console.log(responseData);
      }, error => {
        this.error.next(error.message);
      });
  }

CatchError Operator

CatchError is a special operator assists you handling errors.

fetchPosts() {
    return this.http
      .get<{ [key: string]: Post }>('https://recipebook-cc2b1.firebaseio.com/posts.json')
      .pipe(map(responseData => {
        const postsArray: Post[] = [];
        for (const key in responseData) {
          if (responseData.hasOwnProperty(key)) {
            postsArray.push({...responseData[key], id: key});
          }
        }
        return postsArray;
      }),
        catchError(errorRes => {
          return throwError(errorRes);
        })
    );
  }

HTTP Headers

Any Http request method, GET, POST, and so on, has a last parameter, which is a object that we can configure this request. For example, we can set any key-value pair in our http headers.

fetchPosts() {
    return this.http
      .get<{ [key: string]: Post }>(
        'https://recipebook-cc2b1.firebaseio.com/posts.json',
        {
          headers: new HttpHeaders({'Custom-Header': 'Hello'}),
           
        }
       )
      .pipe(
        map(responseData => {
          const postsArray: Post[] = [];
          for (const key in responseData) {
            if (responseData.hasOwnProperty(key)) {
              postsArray.push({...responseData[key], id: key});
            }
          }
          return postsArray;
        }),
        catchError(errorRes => {
          return throwError(errorRes);
        })
    );
  }

Http query params

We can set our query parameters in our http requests.

params: new HttpParams().set('print', 'pretty')

Of course, we could have append the parameters after our URLs manually. This is just another way to setting parameters.

let searchParams = new HttpParams();
    searchParams = searchParams.append('print', 'pretty');
    searchParams = searchParams.append('custom', 'key');

    return this.http
      .get<{ [key: string]: Post }>(
        'https://recipebook-cc2b1.firebaseio.com/posts.json',
        {
          headers: new HttpHeaders({'Custom-Header': 'Hello'}),
          params: searchParams
        }
        )

Observing different types of response

Observing ‘response’

We can also get the whole response instead of only the response body.

.post<{ name: string }>(
        'https://recipebook-cc2b1.firebaseio.com/posts.json',
        postData,
        {
          observe: 'response'
        }
      )

In this case, we are observing the whole ‘response’, the default was ‘body’. In this way, we can easily get the status of our requests and some other information.

Observing ‘events’

We can use another operator ‘tap’, which allows us to execute some code without altering the response. So we can do something with the response, but not disturb our subscribe function.

We can check whether we got the response.

.pipe(
  tap(event => {
    console.log(event);
    if (event.type === HttpEventType.Sent) {
      // do something
    }
    if (event.type === HttpEventType.Response) {
      console.log(event.body);
    }
  })
);

Change the response body type

We can tell Angular what the response body type would be. By default it is JSON, but we can set to text manually.

.delete(
      'https://recipebook-cc2b1.firebaseio.com/posts.json',
      {
        observe: 'events',
        responseType: 'text'
      }

Interceptors

For example, we want to attach one http header to all http requests, maybe we want to authenticate a user, or maybe we need to add a certain parameter to every request. In these cases, we can add interceptors.

The interceptors will run code before the requests leave the app, and before the responses are forwarded to subscribe.

import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';

export class AuthInterceptorService implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Request is on its way');
    return next.handle(req);
  }
}
providers: [{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true}],

Manipulating request objects

Inside the interceptor, we can also modify the request object. However, the request object itself is immutable. So we cannot set a new url directly. We have to call req.clone().

export class AuthInterceptorService implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Request is on its way');
    console.log(req.url);
    const modifiedRequest = req.clone({
      headers: req.headers.append('Auth', 'xyz')
    });
    return next.handle(modifiedRequest);
  }
}

Now, every request will have one more header: Auth: xyz. This is super useful.

Response interceptors

We can also do something with the response by adding something after the handle method, because handle gives us an Observable.

return next.handle(modifiedRequest).pipe(tap(event => {
      console.log(event);
      if (event.type === HttpEventType.Response) {
        console.log('Response arrived, body data: ');
        console.log(event.body);
      }
    }));

Multiple interceptors

We add another interceptor.

export class LoggingInterceptorService implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Outgoing request: ');
    console.log(req.url);
    return next.handle(req).pipe(tap(event => {
      if (event.type === HttpEventType.Response) {
        console.log('Response arrived, body data: ');
        console.log(event.body);
      }
    }));
  }
}

Now, the order of interceptors in app.module.ts is important.

providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptorService,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LoggingInterceptorService,
      multi: true
    }
  ],
tags: Angular