1. HTTP in angular
1.1. Handling Http Errors in service
- inject
HttpClientModule
in service's constructor - Encapsulate Http errors in service
- Don't expose implementation details to the component
- Use RxJS "catchError" operator
- Return custom errors to components
1.2. Model for error
// Book return error model
export class BookTrackerError {
errorNumber: number;
message: string;
friendlyMessage: string;
}
1.3. Service demo
// book.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { map, tap, catchError } from 'rxjs/operators';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { Book } from 'app/models/book';
import { BookTrackerError } from 'app/models/bookTrackerError';
import { FormattedBook } from 'app/models/formattedBook';
@Injectable()
export class BookService {
constructor(private http: HttpClient) {}
private handleHttpError(error: HttpErrorResponse): Observable<BookTrackerError> {
let dataError = new BookTrackerError();
dataError.errorNumber = 100;
dataError.message = error.statusText;
dataError.friendlyMessage = 'An error occurred retrieving data.';
return ErrorObservable.create(dataError);
}
getAll(): Observable<Book[] | BookTrackerError> {
return this.http.get<Book[]>(`/api/books`).pipe(catchError((err) => this.handleHttpError(err)));
}
// second param is optional
getOne(id: number): Observable<Book> {
return this.http.get<Book>(`/api/books/${id}`, {
headers: new HttpHeaders({
Accept: 'application/json',
Authorization: 'my-token',
}),
});
}
// if UI needs a formatted book
getOneFormatted(id: number): Observable<FormattedBook> {
return this.http.get<Book>(`/api/books/${id}`).pipe(
// data convert
map(
(b) =>
<FormattedBook>{
title: b.bookTitle,
year: b.publicationYear,
},
),
// use converted data to do some logic
tap((newBook) => console.log(newBook)),
);
}
// return 201 if successful
add(book: Book): Observable<Book> {
return this.http.post<Book>(`/api/books`, book, {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
// third param is optional
});
}
// return 204 no content if successful
update(book: Book): Observable<void> {
return this.http.put<void>(`/api/books/${book.id}`, book, {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
// third param is optional
});
}
// return 204 no content if successful
deleteBook(id: Book): Observable<void> {
return this.http.put<void>(`/api/books/${id}`);
}
}
1.4. Component that consumes service
// component using the service
ngOnInit() {
this.bookService.getAll().subscribe(
(data: Book[]) => this.books = data,
(err: BookTrackerError) => console.log(err.friendlyMessage),
() => console.log('all done')
)
}
1.5. Use a resolver
If our component doesn't call bookService in ngOnInit to load data, but use a resolver.
// books-resolver.service.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';
import { Book } from 'app/models/book';
import { BookService } from 'app/core/book.service';
import { BookTrackerError } from 'app/models/bookTrackerError';
@Injectable()
export class BooksResolverService implements Resolve<Book[] | BookTrackerError> {
constructor(private bookService: BookService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<Book[] | BookTrackerError> {
return this.bookService.getAll().pipe(catchError((err) => of(err)));
}
}
1.6. Interceptors
- Adding headers to all requests
- Login
- Reporting progress events
- Client-side caching
Providing an interceptor:
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AddHeaderInterceptor } from './add-header.interceptor';
import { LogResponseInterceptor } from 'app/core/log-response.interceptor';
import { CacheInterceptor } from './cache.interceptor';
@NgModule({
imports: [],
declarations: [],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: LogResponseInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: AddHeaderInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true },
],
})
export class CoreModule {}
// log response interceptor
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
HttpEventType,
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators';
@Injectable()
export class LogResponseInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log(`LogResponseInterceptor - ${req.url}`);
return next.handle(req).pipe(
tap((event) => {
if (event.type === HttpEventType.Response) {
console.log(event.body);
}
}),
);
}
}
// add-header.interceptor
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class AddHeaderInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log(`AddHeaderInterceptor - ${req.url}`);
let jsonReq: HttpRequest<any> = req.clone({
setHeaders: { 'Content-Type': 'application/json' },
});
return next.handle(jsonReq);
}
}
1.6.1. Caching with interceptors
client --> interceptor (check cache) --> no ? --> server
1.6.2. Role of cache service
- Provide a data structure for the cached items
- add items to the cache
- retrieve items from the cache
- remove items from the cache (cache invalidation)
// http-cache.service.ts
import { Injectable } from '@angular/core';
import { HttpResponse } from '@angular/common/http';
@Injectable()
export class HttpCacheService {
private requests: any = {};
constructor() {}
put(url: string, response: HttpResponse<any>): void {
this.requests[url] = response;
}
get(url: string): HttpResponse<any> | undefined {
return this.requests[url];
}
invalidateUrl(url: string): void {
this.requests[url] = undefined;
}
invalidateCache(): void {
this.requests = {};
}
}
// cache interceptor
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
HttpResponse,
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';
import { HttpCacheService } from 'app/core/http-cache.service';
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
constructor(private cacheService: HttpCacheService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// pass along non-cacheable requests and invalidate cache
if (req.method !== 'GET') {
console.log(`Invalidating cache: ${req.method} ${req.url}`);
this.cacheService.invalidateCache();
return next.handle(req);
}
// attempt to retrieve a cached response
const cachedResponse: HttpResponse<any> = this.cacheService.get(req.url);
// return cached response
if (cachedResponse) {
console.log(`Returning a cached response: ${cachedResponse.url}`);
console.log(cachedResponse);
return of(cachedResponse);
}
// send request to server and add response to cache
return next.handle(req).pipe(
tap((event) => {
if (event instanceof HttpResponse) {
console.log(`Adding item to cache: ${req.url}`);
this.cacheService.put(req.url, event);
}
}),
);
}
}