A Generic HTTP Service For Angular

2

Personally I work as a software consultant specializing in Angular. Companies hire me to improve and expand the code base of a particular application. One of the first things I do, is look for repetitive code and create a generic approach. A place where you find this in most Angular applications is within the HTTP services making API requests.

If you ever worked on an Angular application chances are you created one API service for every data model. In that case a file structure looking something like this would be quit familiar to you:

  • API-Services
    • userApiService.service.ts
    • buildingApiService.service.ts
    • appartmentApiService.service.ts
    • and so on

A basic version of one of these services would look something like this:

export class BuildingApiService {

  constructor(private http: HttpClient) {}

  public create(building: BuildingDto): Observable<BuildingDto> {
    return this.http.post<BuildingDto>('http://someapiurl/building', building);
  }

  public update(building: BuildingDto): Observable<BuildingDto> {
    return this.http.put<BuildingDto>('http://someapiurl/building', building);
  }

  getById(id: number): Observable<BuildingDto> {
    return this.http.get<BuildingDto>(`http://someapiurl/building/${id}`);
  }

  getAll(): Observable<BuildingDto[]> {
    return this.http.get<BuildingDto[]>('http://someapiurl/building');
  }

  deleteById(id: number) {
    return this.http.delete(`http://someapiurl/building/${id}`);
  }
  
}

You quickly see the problem here. All of these API services basically will have the same code. The only differences are the API routes and object types. In some cases you want to return a  view model that is different fromyour data transfer object (DTO). These 3 things should be the only variation within these files. So this seems like a good place to reduce the code base, save some work by creating less boilerplate and increase the maintainability of your application.

To do all of this we need a generic HTTP service for our Angular application. This generic HTTP service will handle all HTTP request but still ensures that requests are type safe. It will also have the ability to return a different view model when you want to (and I will explain later why you always want to do this).

In most cases if you want to create a generic solution in Angular you need to make use of Typescript generics. This is because we want to keep our generic solution type safe and Typescript generics allows us to do this. If you are not familiar with Typescript generics don’t worry. At the end of this article you will have a good grasp on how generics work. You will be able to create a generic HTTP service with Typescript generics and use them for other problems you face in your application (if you want to read more about Typescript generics you can go to the official Typescript documentation about generics).

Basically generics allow you to accept a type as a parameter. You probably used them more often then you realize. For example every time you add a type between the arrows of an Observable<somType> ; you pass it along as a parameter for the generic type of the Observable class.

Accepting a generic type for your class can be done by adding a <T> after the class name. Everywhere you want to use this generic type within your class you can write a T as a type parameter. Lets dive back into the code and see how we can create a simple generic HTTP service for our Angular application. We will keep everything type safe by using Typescript generics.

export class GenericHttpService<T> {
	constructor(
		private httpClient: HttpClient,
		private url: string,
		private endpoint: string
		) {}

	  public create(item: T): Observable<T> {
		return this.httpClient.post<T>(`${this.url}/${this.endpoint}`, item);
	  }

	  public update(item: T): Observable<T> {
		return this.httpClient.put<T>(`${this.url}/${this.endpoint}/${item.id}`, item);
	  }

	  getById(id: number): Observable<T> {
		return this.httpClient.get(`${this.url}/${this.endpoint}/${id}`);
	  }

	  getAll(): Observable<T[]> {
		return this.httpClient.get(`${this.url}/${this.endpoint}?${queryOptions.toQueryString()}`);
	  }

	  deleteById(id: number) {
		return this.httpClient.delete(`${this.url}/${this.endpoint}/${id}`);
	  }
}

A generic type is added next to the class name as you can see in the code snippet above. This indicates that this class takes a type as a parameter. You can also find this T in several places within the class. Here the T indicates that the object will be of the generic type that we pass along to this class.

Now lets refactor our building API service into a version that works with the generic HTTP service we just created.

export class BuildingApiService extends GenericHttpSerivce<BuildingDto> {
  constructor(httpClient: HttpClient) {
    super(
      httpClient,
      'http://your-api-url/',
      'building',);
  }
}

It’s directly apparent that we needed a lot less code for this service. We removed all functions and let the generic HTTP service handle this. Creating new API services will be a piece of cake now. They will have a lot less duplicate code and are much easier to generate automatically. You might be asking what did we do here and how do the generic types come into play?

First we extended the BuildingApiService with the GenericHttpSerivce (if you are not familiar with OOP, extending a class basically gives you access to all the public properties and classes of the class you extend). Beside extending the generic HTTP service, we also give it the BuildingDto as a type parameter between the arrows. This is the input for our generic type. The GenericHttpService will take this buildingDto and uses it everywhere we placed the T in the generic HTTP service class.

If we go on, you can see we inject the HttpClient into our constructor. This time we pass the HttpClient along to our generic HTTP service constructor within our super() call. The constructor of our Generic HTTP service also aspects to receive a URL and endpoint that can be used for all the requests. Inside the super() method you find these as parameters.

Now you have a working BuildingApiService that uses the generic HTTP service. It works fine but we can still make a few improvements. We still need a way to convert objects from the DTO representation to the view model representation.

There are several ways of doing this, but personally I like to use the model adapter pattern for this. Even if your view models and DTO’s are exactly similar to each other it’s a good practice to include the model adapter pattern and change your objects into view representations. If you implement this pattern it will save you a lot of work down the line. Now if the backend team changes a property name on one of the DTO’s, you only have to change it in one place and not everywhere you used this property in your application.

Adapting models in our generic HTTP service

If we want to adapt our models, we need to create a model adapter for every API service we create. So let’s take the BuildingApiService as an example again. Lets say we want to adapter our BuildingDto into a BuildingViewModel. To do this we first create a building model adapter and a class for the view model representation.


export class BuildingModelAdapter {
	  fromDto(dto: BuildingDto): BuildingViewModel {
		const building = new BuildingViewModel();
		building.id = dto.id;
		building.buildingName = dto.buildingName;
		building.info = dto.info;
		building.infoRequestedOn = new Date();

		return building;
	  }

	  toDto(building: BuildingViewModel): BuildingDto {
		return {
		  id: building.id,
		  buildingName: building.buildingName,
		  info: building.info
		};
	  }
}

export class BuildingViewModel {
	id: number;
	buildingName: string;
	info: buildingInfo;
	infoRequestedOn: Date;
}

Once we created this we can use it to change our models from a DTO into a view model. We can also turn a view model back into a DTO. Now we only need a way to include this logic in our generic HTTP service. Then all the model adapting can be done automatically when you make API requests.  The API will receive the DTO and in the client you will have the view models. Lets start on that by creating an interface for the model adapters.

export interface IModelAdapter {
  fromDto(Dto);
  toDto(viewModel);
}

Now we can add it to the constructor or our generic HTTP service and keep it type safe. After you added it to the constructor you can use it throughout the generic HTTP service to adapt the objects from and to the view model and DTO representation of itself.

export class GenericHttpService<T> {
	constructor(
		private httpClient: HttpClient,
		private url: string,
		private endpoint: string
		private modelAdapter: IModelAdapter
		) {}

	  public create(item: T): Observable<T> {
		return this.httpClient.post<T>(`${this.url}/${this.endpoint}`, this.modelAdapter.toDto(item)).pipe(map(data => this.modelAdapter.fromDto(data) as T));
	  }

	  public update(item: T): Observable<T> {
		return this.httpClient.put<T>(`${this.url}/${this.endpoint}/${item.id}`, this.modelAdapter.toDto(item)).pipe(map(data => this.modelAdapter.fromDto(data) as T));
	  }

	  getById(id: number): Observable<T> {
		return this.httpClient.get(`${this.url}/${this.endpoint}/${id}`).pipe(map(data => this.modelAdapter.fromDto(data) as T));
	  }

	  getAll(): Observable<T[]> {
		return this.httpClient.get(`${this.url}/${this.endpoint}?${queryOptions.toQueryString()}`).pipe(map((data: any) => this.convertData(data.items)));
	  }

	  deleteById(id: number) {
		return this.httpClient.delete(`${this.url}/${this.endpoint}/${id}`);
	  }

	  private convertData(data: any): T[] {
    	  return data.map(item => this.modelAdapter.fromDto(item);
  	  }
}

After we added the model adapter logic to the generic HTTP service, all that is left is to adjust the building API service. The building API service should include the building model adapter and it’s view model.

export class BuildingApiService extends GenericHttpSerivce<BuildingViewModel> {
  constructor(httpClient: HttpClient) {
    super(
      httpClient,
      'http://your-api-url/',
      'building',
      new BuildingModelAdapter()
	);
  }
}

Now when we receive data in the client it will have the building view model properties and when we send data back to the API it will have the DTO representation of itself. If the backend team now wants to change the property name building.info into building.buildingInfo, you only have to change it in your building model adapter and not everywhere in your app where you used this property. As you can see implementing this model adapter pattern improves maintainability a lot and is worth the extra time it takes to create the model adapter files.

But we can still improve the generic HTTP service a bit. We still have a few any types I want to replace. We will be doing this by added an extra generic type to the generic HTTP service. This is just a small bonus, but there might be other cases where you need to have more than one generic type, so I wanted to show you how easy it is to introduce an extra generic type.

export class GenericHttpService<T, U> {
	constructor(
		private httpClient: HttpClient,
		private url: string,
		private endpoint: string
		private modelAdapter: IModelAdapter
		) {}

	  public create(item: T): Observable<T> {
		return this.httpClient.post<T>(`${this.url}/${this.endpoint}`, this.modelAdapter.toDto(item)).pipe(map(data => this.modelAdapter.fromDto(data) as T));
	  }

	  public update(item: T): Observable<T> {
		return this.httpClient.put<T>(`${this.url}/${this.endpoint}/${item.id}`, this.modelAdapter.toDto(item)).pipe(map(data => this.modelAdapter.fromDto(data) as T));
	  }

	  getById(id: number): Observable<T> {
		return this.httpClient.get(`${this.url}/${this.endpoint}/${id}`).pipe(map(data => this.modelAdapter.fromDto(data) as T));
	  }

	  getAll(): Observable<T[]> {
		return this.httpClient.get(`${this.url}/${this.endpoint}?${queryOptions.toQueryString()}`).pipe(map((data: U) => this.convertData(data.items)));
	  }

	  deleteById(id: number) {
		return this.httpClient.delete(`${this.url}/${this.endpoint}/${id}`);
	  }

	  private convertData(data: U): T[] {
    	  return data.map(item => this.modelAdapter.fromDto(item);
  	  }
}

You can just add an other letter next to the T and that indicated that you will accept an other generic type for this class. It is good practice to take the next letter in the alphabet, so after the T we take the U, but basically any letter will do.  Now you only need to update the BuildingApiService and everything is finished and type safe.

export class BuildingApiService extends GenericHttpSerivce<BuildingViewModel, BuildingDto> {
  constructor(httpClient: HttpClient) {
    super(
      httpClient,
      'http://your-api-url/',
      'building',
      new BuildingModelAdapter()
	);
  }
}

You can still improve on this generic HTTP service for your Angular application, but this is a good base to start from and a nice working version that is fine for most use cases and completely type safe. If you have any questions please ask it in the comments section and I will get back to you as soon as possible.

 

 

 

 

Share.

2 Comments

Leave A Reply