Angular CRUD service: Create it, Extend it

Additional Notes on increasing resiliency with Delayed Retrying, Error Handling

Jeni Joe
4 min readJan 4, 2021

The Angular CLI is one of the most useful aspects of the Angular framework. It generates a lot of the initial boilerplate code, which can then be modified according to need.

To set up your local environment with Angular, refer: Setting up the local environment and workspace

Let's follow the best of angular’s coding practices for services:

Single responsibility:

Do one thing per service. Limit functions to 75 lines of code.

Naming:

Do use consistent names for all services named after their feature. Do suffix a service class name with Service. The prefix should indicate what the service is responsible for.

Example: We need a service that lets us interact with a list of vehicles. In order to attain this following Angular’s best practices:

  1. We would first create a data service for performing CRUD (Create Read Update Delete) operations. This would be agnostic of what kind of data it would be used for.
  2. Create VehicleService.ts that extends DataService.ts and implements its methods

The benefit is that now if we need to interact with a specific kind of vehicle with added functionality, we can extend VehicleService.ts, or if we need to interact with a different data set altogether, we can extend DataService.ts

Our code is thus reusable, maintainable, and easy to test.

Links to articles on testing
1. Unit testing Angular CRUD Service with Jasmine
2. Angular Unit Testing with Jasmine: Code Snippets

Now, let's look at some code.

It's easy to generate a service using Angular CLI.

ng generate service data-service

The command above will generate 2 files: data.service.ts (DataService)and data.service.spec.ts

Modifying DataService

Modify the constructor generated by the CLI as follows. Add functions for GET, POST, PUT, DELETE HTTP requests, with custom HTTP Headers as required.

import { HttpClient, HttpHeaders } from '@angular/common/http';let HTTP_OPTIONS;@Injectable()
export class DataService {
/**
* @param url URL string passed from service class that extends
* DataService.ts
* @param http HTTPClient service instance that enables making HTTP
* calls
*/
constructor(@Inject('url') private url: string, private http: HttpClient) {
HTTP_OPTIONS = {
headers: new HttpHeaders(
{ "HeaderNameA": "HeaderValueA",
"HeaderNameB": "HeaderValueB"
})
}
}
/**
* Fetches all records
*/
getAll() {
//Adding additional headers custom to type of HTTP request if needed
let httpOptions = HTTP_OPTIONS;
httpOptions.headers = httpOtions.headers.append("AdditionalHeaderName","Value");
return this.http.get(this.url, httpOptions);
}
/**
* Fetches record by id
* @param id
*/
getById(id) {
return this.http.get(this.url+'/'+id, HTTP_OPTIONS);
}
/**
* Creates a record using POST HTTP request
* @param resource Object which will be created
*/
create(resource) {
return this.http.post(this.url, resource, HTTP_OPTIONS);
}
/**
* Updates a record using PUT HTTP request
* @param resource updated Object
* @param id id for object to be updated
*/
update(resource, id) {
return this.http.put(this.url+'/'+id, resource, HTTP_OPTIONS);
}
/**
* Deletes record by id
* @param id
*/
delete(id) {
return this.http.delete(this.url+'/'+id, HTTP_OPTIONS);
}
}

Delayed Retrying of requests:

Additionally, we can add a delayed retry method which accepts custom values for number of times to retry the HTTP request in case of failure and the delay in milliseconds between successive attempts in order to increase resiliency.

Refer to the following article for delayed retrying from: https://medium.com/angular-in-depth/retry-failed-http-requests-in-angular-f5959d486294

Sample update in code using delayedRetries, which would have to be imported into DataService.ts, and then used as follows:

return this.http.get(this.url, HTTP_OPTIONS).pipe(delayedRetries(2000, 3));

Error Handling:

I created a custom Interceptor for inspecting all calls, with the intent of keeping all error handling logic in one place. Error handling can also be done in DataService

Additionally, we can display a custom message to users according to the type of error that occurred on a modal.

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
/**
* ErrorDialogDisplayService is a custom service that accepts a user
* friendly string argument and displays that on a custom Angular
* Material Dialog modal to the user when an error occurs
* This is optional
*/
constructor( private errorDialog: ErrorDialogDisplayService ) { }
/**
* Intercepts all HTTP calls
*/
intercept( request: HttpRequest<any>, next: HttpHandler) {
return next.handle(request).pipe(
map((event: HttpEvent<any>) => {
//optionally log event
return event;
}),
catchError((error: HttpErrorResponse) => {
if(error.status == 400) {
this.errorDialog.showDialog("Bad Request. Please try again");
return throwError(error);
}
if(error.status == 404) {
this.errorDialog.showDialog("Not Found!");
return throwError(error);
}
if(error.status == 503) {
this.errorDialog.showDialog("Service currently unavailable. Please try again");
return throwError(error);
}
}));
}
}

Custom service that extends DataService:

We can have different services extending DataService for different API endpoints. These services can then be imported and used in components.

/**
* Add @Injectable decorator on a service if this service has
* dependencies itself
*/
@Injectable({
providedIn: 'root'
})
export class VehicleService extends DataService {
/**
* Performs dependency injection
* @param url URL string
*/
constructor(http: HttpClient) {
super(url, http);
}
}

--

--