Generic CRUD Service & Models in Angular

Nikos Anifantis
9 min readOct 23, 2021

--

Photo by Randy Fath on Unsplash

Introduction ⛳️

In this article, we are going to learn how to create generic models and services step-by-step regarding the CRUD feature of resources in Angular.

In a nutshell, we’re going to learn:

  • How to use generics in Typescript
  • What is CRUD operations in APIs
  • How to create generic models in Angular
  • How to create generic service for CRUD in Angular

This article is a little bit loooong. So, grab a cup of coffee ☕️ and sit comfortably… 😄

The Problem 🤔

One of the most important principles in programming is the “Don’t-Repeat-Yourself” (DRY) principle when it comes to writing dynamic and reusable code. We encounter this problem in the main functions provided by the models in real-world applications. More specifically, when we are dealing with API endpoints a model can either be created, read, updated, or deleted.

For example, let’s say that we have two models in our app, “article” and “author”. When we are faced with implementing the basic methods requesting the server’s API for both models, we are must implement the same functionality twice. Thankfully, Typescript supports generics and allows us to avoid code duplications.

Understanding Generics 📕

First things first! Before we start to analyze our main content, we must understand how does generics work in Typescript and the definition of CRUD in APIs.

Generics are awesome! It allows us to keep our code clean and reusable avoiding duplications. I believe that whoever has used it with Typescript, loves it. The rest of you will love it after this article 😆.

➡️ Example 1 — Type-Safe Generics

The first example that we will see, presents a method where it takes as arguments 2 parameters and returns an object based on them. It is a very simple method but with powerful types.

As you can see below, we use T and U as generic types which enforce both arguments and returned type to be the same type.
If we pass as the first argument a value of type string, we are able to know that the property value1 of the result will be string as well. Maybe it looks like a little bit dummy but you can imagine a real-world example (e.g. the main topic of this article - generic CRUD service).

Furthermore, if we look closer at the following example, we can predefine the returned object by adding objectify<string, number>(...). Thus, we enforce the first argument to be of the type string and the second of type number. If we try to pass a different type (e.g boolean), then we'll have a type error.

➡️ Example 2 — Generic Classes

The second example concerns class-es. Yes, classes! Generics work not only with functions but for class, interface, even type. In this example, we will create a custom class implementing a simple functionality of the array. It can be initialized by a given array of items, add a new item, and get all items.

We use T as generic type to declare the type of the stored items in our custom class. Thus, we ensure that all items will be the same type. For example, if we add the following new MyCustomArray<number>(), we restrict our instance to accept only values of type number at addItem() method. Also, we can infer that the getItems() method will return a list of values of type number.

➡️ Example 3 — Generic Constraints

In our third example, we’ll learn how to add constraints at generic types. Below you will find a method that echoes a value, it accepts an argument and returns it as is. But the main difference is that the value must be either of type string or number. We use T generic type and we extend it to be string | number.

Thus, if we try to write something like echo<boolean>(true), Typescript will throw a type error. This functionality is very useful because sometimes our implementation requires specific criteria and we can limit the accepted values.

What is CRUD❓

Create, Read, Update, and Delete 👉 CRUD

Starting from the acronym, CRUD means create, read, update, and delete. These are the four main functionalities that must be provided by all models when building APIs.

Firstly let’s explain some basics… An API is a set of definitions and protocols for building and integrating application software. It’s the main contract between the frontend and backend regarding their communication. An API in order to be RESTful, should follow the constraints of REST architectural style and allow for interaction with RESTful web services. REST stands for representational state transfer and was created by computer scientist Roy Fielding.

So, talking about RESTful APIs, the CRUD feature usually corresponds to the HTTP methods POST, GET, PUT, and DELETE, respectively. These are the basic elements of a persistent storage system.

Note: Keep in mind that this article recommends the most common approaches followed by developers for CRUD regarding responses, status codes, endpoints’ paths etc. Follow this article for inspiration to create your rules according your needs. 🚀

Model Example

This article examines a very common model for most of the real-world applications, the model of User.

We simplify the model with the following properties:

{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john@email.com",
"createdAt": "2021-09-22T16:21:47.760Z",
"updatedAt": "2021-09-22T16:21:47.851Z"
}

In this hypothetical database, let’s assume that the id, createdAt, and updatedAt properties are handled only from our server's API. It's not important for now, but we'll notice in the following sections that all models share the aforementioned properties.

➡️ Create

If we want to create a new user in our system, we use the POST method and the endpoint path should start with the base followed by the model name (usually in the plural). The response should return 201 - Created status code.

Operation: POST
Endpoint: /api/users
Status Code: 201 (created)
--------------------------------------
Payload:
{
"firstName": "John",
"lastName": "Doe",
"email": "john@email.com"
}
--------------------------------------
Response:
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john@email.com",
"createdAt": "2021-09-22T16:21:47.760Z",
"updatedAt": "2021-09-22T16:21:47.851Z"
}

➡️ Read

In order to retrieve all existing models from our database, we request with GET method at the base path of the user model. It's very similar to the previous endpoint, but here we changed the POST to GET. Also, this method accepts an empty payload as we cannot change users in our database. Finally, we get a list of users as response with 200 - OK status code.

Operation: GET
Endpoint: /api/users
Status Code: 200 (OK)
--------------------------------------
Payload: None
--------------------------------------
Response:
[
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john@email.com",
"createdAt": "2021-09-22T16:21:47.760Z",
"updatedAt": "2021-09-22T16:21:47.851Z"
}
{
"id": 2,
"firstName": "John",
// ...
}
]

Another use case is when we want to retrieve only one specific user by ID. Then, we call the same base model’s path, but we add its ID at the end. Another difference is that instead of getting a list of users, the response returns an object with the found user.

Operation: GET
Endpoint: /api/users/{id}
Status Code: 200 (OK)
--------------------------------------
Payload: None
--------------------------------------
Response:
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john@email.com",
"createdAt": "2021-09-22T16:21:47.760Z",
"updatedAt": "2021-09-22T16:21:47.851Z"
}

➡️ Update

The “update” functionality is used when we want to modify an existing user. We recommend sending only the values that we want to be updated to the server. The endpoint’s path is similar to “read by ID”, but we use the PUT method. The response should include the updated version of the user, followed by a 200 - OK status code.

Operation: PUT
Endpoint: /api/users/{id}
Status Code: 200 (OK)
--------------------------------------
Payload:
{
"firstName": "Nikos"
}
--------------------------------------
Response:
{
"id": 1,
"firstName": "Nikos", // <-- Changed
"lastName": "Doe",
"email": "john@email.com",
"createdAt": "2021-09-22T16:21:47.760Z",
"updatedAt": "2021-09-23T12:13:07.651Z" // <-- Changed from server
}

➡️ Delete

Last but not least, we have the “delete” functionality which is used to delete an existing user by ID. Again the path is the same when reading/updating a model, but we use the DELETE method. Another important point here is that neither the payload nor the response transfers any data.

Operation: DELETE
Endpoint: /api/users/{id}
Status Code: 204 (No Content)
--------------------------------------
Payload: None
--------------------------------------
Response: None

Generic CRUD Model 💡

Once we have walked about the theory of generics and we have understood the main CRUD methods, now it’s time to see it in action.

Before starting to implement the generic service of CRUD. We must define the generic model of all resources.

We assume that all models have some common properties like id, createdAt, and updatedAt. Thus, we create a generic abstract class that contains all these common properties. Also, we used the generic T type to identify the model that we are going to extend.

Last but not least, the class is going to apply logic in the constructor about property casting and provide a common method toJson() which allows us to return a pure JSON based on the instance.

Below we can see the full implementation of the generic CRUD model:

Example

Do you remember the example of the user model? Great! The same model will be used here. Below there is an example of how we can extend the user model:

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.

➡️ Create

The create() method accepts a partial model as an argument and returns the created model from the 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 the 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 as toJson() 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)));
}

➡️ 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))));
}

➡️ Get by ID

The next method of “read” is getById(). As is obvious, it accepts as an 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)));
}

➡️ 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)));
}

➡️ Delete

Finally, the delete() method removes completely an existing resource from the server by a given ID. It accepts a number as an 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:

Finally, here is a working example of users’ service:

Conclusion ✅

Hooray! We made it to the end! 🙌

I hope you enjoyed this 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 clap 👏 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 social media at Twitter DMs @nikosanif, LinkedIn, and GitHub.

You can find the final source code in stackblitz:

Originally published at dev.to.

--

--

Nikos Anifantis
Nikos Anifantis

Written by Nikos Anifantis

Full Stack Engineer. #Development addict. Enthusiast in #WebDev.

Responses (1)