I\'m new to typescript and angular2/4 and I\'m building a single app that have two basic entities which is Car and Driver and all I do is to list them with an API call.
Below is a basic example built on Angular 7 and RxJS 6.
ApiResponse
represents any server response. Server must have the same structure and return it whatever happens:
export class ApiResponse {
constructor() {
this.errors = [];
}
data: T;
errors: ApiError[];
getErrorsText(): string {
return this.errors.map(e => e.text).join(' ');
}
hasErrors(): boolean {
return this.errors.length > 0;
}
}
export class ApiError { code: ErrorCode; text: string; }
export enum ErrorCode {
UnknownError = 1,
OrderIsOutdated = 2,
...
}
Generic service:
export class RestService {
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json',
'Accept': 'application/json'})
};
private _apiEndPoint: string = environment.apiEndpoint;
constructor(private _url: string, private _http: HttpClient) { }
getAll(): Observable> {
return this.mapAndCatchError(
this._http.get>(this._apiEndPoint + this._url
, this.httpOptions)
);
}
get(id: number): Observable> {
return this.mapAndCatchError(
this._http.get>(`${this._apiEndPoint + this._url}/${id}`
, this.httpOptions)
);
}
add(resource: T): Observable> {
return this.mapAndCatchError(
this._http.post>(
this._apiEndPoint + this._url,
resource,
this.httpOptions)
);
}
// update and remove here...
// common method
makeRequest(method: string, url: string, data: any)
: Observable> {
let finalUrl: string = this._apiEndPoint + url;
let body: any = null;
if (method.toUpperCase() == 'GET') {
finalUrl += '?' + this.objectToQueryString(data);
}
else {
body = data;
}
return this.mapAndCatchError(
this._http.request>(
method.toUpperCase(),
finalUrl,
{ body: body, headers: this.httpOptions.headers })
);
}
/////// private methods
private mapAndCatchError(response: Observable>)
: Observable> {
return response.pipe(
map((r: ApiResponse) => {
var result = new ApiResponse();
Object.assign(result, r);
return result;
}),
catchError((err: HttpErrorResponse) => {
var result = new ApiResponse();
// if err.error is not ApiResponse e.g. connection issue
if (err.error instanceof ErrorEvent || err.error instanceof ProgressEvent) {
result.errors.push({ code: ErrorCode.UnknownError, text: 'Unknown error.' });
}
else {
Object.assign(result, err.error)
}
return of(result);
})
);
}
private objectToQueryString(obj: any): string {
var str = [];
for (var p in obj)
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
return str.join("&");
}
}
then you can derive from RestService
:
export class OrderService extends RestService {
constructor(http: HttpClient) { super('order', http); }
}
and use it:
this._orderService.getAll().subscribe(res => {
if (!res.hasErrors()) {
//deal with res.data : Order[]
}
else {
this._messageService.showError(res.getErrorsText());
}
});
// or
this._orderService.makeRequest('post', 'order', order).subscribe(r => {
if (!r.hasErrors()) {
//deal with r.data: number
}
else
this._messageService.showError(r.getErrorsText());
});
You can redesign RestService
and inject RestService
directly instead of declaring and injection OrderService
.
It looks like RxJS 6
doesn't allow to rethrow/return typed errors. For this reason RestService
catches all errors and returns them within strongly typed ApiResponse
. The calling code should check ApiResponse
instead of catching errors on Observable