Cover Photo by Xan Griffin on Unsplash.
This article is a part of a series about how to create Generic CRUD Service & Models in Angular:
- Part 1 - Understanding Generics
- Part 2 - What is CRUD?
- Part 3 - Generic CRUD Model
- 👉 Part 4 - Generic CRUD Service in Angular
Generic CRUD Service
Before we start generating and writing code, let’s take a step back and see the bigger picture. The generic service should accept the following arguments:
- the
HttpClient
is used for the HTTP requests - the class of model for creating instances
- the path of the API endpoints.
Also, all resources should have 5 main methods related to CRUD…
- Create - Returns a new resource.
- Get all - Retrieves all resources as a list.
- Get by ID - Returns a specific resource by ID.
- Update - Updates a specific resource by ID.
- Delete - Removes a specific resource by ID.
Great, let’s create our methods step by step now.
1️⃣ Create
The create()
method accepts a partial model as argument and returns the created model from server. We say "partial" because before we create the resource, some properties are not available (e.g. id
, createdAt
, etc). Also, it converts the result to an instance of model's class.
TIP: All methods try to create instances of model's class in order to apply and benefit extra functionality from them (e.g. convert string dates to actual
Date
in constructor or for future usage of their methods such astoJson()
function).
public create(resource: Partial<T> & { toJson: () => T }): Observable<T> {
return this.httpClient
.post<T>(`${this.apiUrl}`, resource.toJson())
.pipe(map((result) => new this.tConstructor(result)));
}
2️⃣ Get all
The get()
method returns an Observable
with a list of all existing resources. It accepts no arguments and iterates the list to create multiple instances instead of simple JSON objects.
public get(): Observable<T[]> {
return this.httpClient
.get<T[]>(`${this.apiUrl}`)
.pipe(map((result) => result.map((i) => new this.tConstructor(i))));
}
3️⃣ Get by ID
The next method of "read" is getById()
. As is obvious, it accepts as argument an ID of type number
and returns an Observable
of the existing resource instance.
public getById(id: number): Observable<T> {
return this.httpClient
.get<T>(`${this.apiUrl}/${id}`)
.pipe(map((result) => new this.tConstructor(result)));
}
4️⃣ Update
When we want to update an existing resource, we'll use the update()
method. It accepts a partial model (e.g. only properties that we want to update) and returns the updated instance as Observable
.
public update(resource: Partial<T> & { toJson: () => T }): Observable<T> {
return this.httpClient
.put<T>(`${this.apiUrl}/${resource.id}`, resource.toJson())
.pipe(map((result) => new this.tConstructor(result)));
}
5️⃣ Delete
Finally, the delete()
method removes completely an existing resource from the server by a given ID. It accepts a number as argument that matches the ID of the model, but it does not return anything (Observable<void>
).
public delete(id: number): Observable<void> {
return this.httpClient.delete<void>(`${this.apiUrl}/${id}`);
}
➡️ Final result
Once we described one-by-one all methods, now it's time to see the final result of the generic service:
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ResourceModel } from 'your-path-to-model'; // see: Part 3
export abstract class ResourceService<T extends ResourceModel<T>> {
constructor(
private httpClient: HttpClient,
private tConstructor: { new (m: Partial<T>, ...args: unknown[]): T },
protected apiUrl: string
) {}
public create(resource: Partial<T> & { toJson: () => T }): Observable<T> {
return this.httpClient
.post<T>(`${this.apiUrl}`, resource.toJson())
.pipe(map((result) => new this.tConstructor(result)));
}
public get(): Observable<T[]> {
return this.httpClient
.get<T[]>(`${this.apiUrl}`)
.pipe(map((result) => result.map((i) => new this.tConstructor(i))));
}
public getById(id: number): Observable<T> {
return this.httpClient
.get<T>(`${this.apiUrl}/${id}`)
.pipe(map((result) => new this.tConstructor(result)));
}
public update(resource: Partial<T> & { toJson: () => T }): Observable<T> {
return this.httpClient
.put<T>(`${this.apiUrl}/${resource.id}`, resource.toJson())
.pipe(map((result) => new this.tConstructor(result)));
}
public delete(id: number): Observable<void> {
return this.httpClient.delete<void>(`${this.apiUrl}/${id}`);
}
}
Finally, here a working example of users' service:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { User } from 'your-path-to-user-model';
import { ResourceService } from 'your-path-to-resource-service';
@Injectable({ providedIn: 'root' })
export class UsersService extends ResourceService<User> {
constructor(private http: HttpClient) {
super(http, User, `your-api-of-users-here`);
}
}
You can find the final source code in stackblitz:
Conclusion ✅
Hooray! We made it to the end! 🙌
I hope you enjoyed this series of article and you will make your applications' code even more generic and reusable following the DRY principle. Also, I hope to use this article not only for the CRUD feature but whenever it's possible in your apps by using generics.
Please support this article (and the previous parts) with your ❤️ 🦄 🔖 to help it spread to a wider audience. 🙏
Also, don’t hesitate to contact me if you have any questions leaving here your comments or Twitter DMs @nikosanif.
Author: Nikos Anifantis ✍️
Top comments (0)