2 July 2019
Angular (19) -- HTTP requests
by Jerry Zhang
Day 26: Angular HTTP requests
Sending a simple POST Request
-
Step 1: In
app.module.ts
, importHttpClientModule
from@angular/common/http
. -
Step 2: Inject HttpClient in the constructor.
constructor(private http: HttpClient) {}
- Step 3: Send http requests
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.
- To send multiple 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.
- Create an interceptor file
auth-interceptor.service.ts
.
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);
}
}
- Add this interceptor into app.module.ts
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().
- Notice that we should forward the modified request this time.
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
}
],