Generic CRUD Service in Angular

A. Waris
6 min readJul 31, 2023

--

When building applications in Angular, one common requirement is to interact with RESTful APIs and perform CRUD (Create, Read, Update, Delete) operations on different data models. As the number of data models increases, the amount of repetitive code for HTTP requests and data handling can become overwhelming. To address this challenge, we can create a powerful and reusable Generic CRUD Service using Angular’s Dependency Injection system and generic types. In this article, we’ll explore how to build and use the GenericService to simplify and optimize your codebase.

Prerequisites

Before we dive into the implementation, ensure you have a basic understanding of Angular and TypeScript. Familiarity with Angular’s HttpClient for making HTTP requests and the Dependency Injection system is also essential.

Building a Generic CRUD Service

The GenericService is designed to handle CRUD operations for various data models using generic types. Let’s take a closer look at the implementation:

import { HttpClient } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';

import { InjectionToken } from '@angular/core';

export interface ServiceConfig {
resourceEndpoint: string;
}

export const SERVICE_CONFIG = new InjectionToken<ServiceConfig>('ServiceConfig');

@Injectable({
providedIn: 'root',
})
export class GenericService<TModel, TDto> {
protected readonly baseUrl: string;
protected readonly resourceEndpoint: string;

constructor(
protected httpClient: HttpClient,
@Inject(SERVICE_CONFIG) config: ServiceConfig
) {
this.baseUrl = environment.serviceUrl;
this.resourceEndpoint = config.resourceEndpoint;
}

getList() {
return this.httpClient.get<TModel[]>(`${this.baseUrl}${this.resourceEndpoint}`);
}

getById(id: number) {
return this.httpClient.get<TModel>(`${this.baseUrl}${this.resourceEndpoint}/${id}`);
}

add(dto: TDto) {
return this.httpClient.post<TModel>(`${this.baseUrl}${this.resourceEndpoint}`, dto);
}

update(dto: TDto) {
return this.httpClient.put<TModel>(`${this.baseUrl}${this.resourceEndpoint}`, dto);
}

remove(id: number) {
return this.httpClient.delete<number>(`${this.baseUrl}${this.resourceEndpoint}/${id}`);
}
}

Explanation:

The GenericService is designed as a base service that can be extended to work with different data models. It makes use of TypeScript’s generic types, TModel and TDto, to accommodate various data structures while ensuring type safety.

Introducing InjectionToken and ServiceConfig

Understanding InjectionToken and ServiceConfig in Angular:

In Angular, Dependency Injection is a crucial concept that enables efficient communication and collaboration among different components and services within an application. Two essential elements in this process are InjectionToken and ServiceConfig. In this article, we will delve into the roles and significance of these constructs, shedding light on their practical implementation and benefits.

InjectionToken:

An InjectionToken in Angular is a powerful tool that serves as a unique identifier for a particular dependency during the dependency injection process. Unlike regular tokens, which can be strings or classes, InjectionTokens offer a more reliable way to reference dependencies, particularly when working with complex or ambiguous dependencies.

When using the Angular Dependency Injection system, developers can specify the InjectionToken as a provider key. This allows the injector to accurately identify and associate the token with its corresponding instance or value, ensuring that the correct dependency is injected into the consuming component or service.

ServiceConfig:

ServiceConfig is closely related to InjectionToken and is often used in conjunction with it. It allows developers to pass configuration data to a service during its instantiation through the Angular Dependency Injection mechanism. This enables services to be more flexible and adaptable, as their behavior can be adjusted based on the provided configuration.

By leveraging ServiceConfig in combination with InjectionToken, developers can fine-tune the behavior of services, making them versatile and suitable for different scenarios without having to create multiple, nearly identical services.

Resource Endpoint: The GenericService expects a ServiceConfig object during instantiation, which contains the resourceEndpoint. This endpoint specifies the URL for the data model's API endpoint. By injecting this configuration using Angular's Dependency Injection, we can easily customize the API endpoint for each instance of the service.

Constructor: The constructor initializes the baseUrl using the environment's service URL and sets the resourceEndpoint from the injected configuration.

CRUD Operations: The service provides generic methods for performing CRUD operations: getList(), getById(id), add(dto), update(dto), and remove(id). These methods use the provided HttpClient instance to interact with the API using the specified URL, making the service flexible and reusable for various data models.

Using the GenericService in a Component:

Once we have the GenericService in place, let’s see how we can use it in a component.

@Component({
// Code...
providers: [
// Approach 1
{
provide: 'usersService',
useFactory: (http: HttpClient) =>
new GenericService<User, UserAddDto | UserUpdateDto>(http, {
resourceEndpoint: '/users',
}),
deps: [HttpClient],
},

// Approch 2
GenericService,
{
provide: SERVICE_CONFIG,
useValue: { resourceEndpoint: '/posts' },
},
],
})
export class MyComponent {
constructor(
private postsService: GenericService<Post, UpdatePostDto | AddPostDto>,
@Inject('usersService') private usersService: GenericService<User, UserAddDto | UserUpdateDto>
) {
// Now you can use `usersService` and `postsService` as needed.
}
}

let’s discuss the two different approaches you can use to inject different instances of a generic service in Angular and their pros and cons:

Approach 1: private usersService:

In this approach, we directly inject the service into the component. It is a more straightforward and simpler approach. We provide the configuration for the service in the providers array of the component, and then we simply inject the service using Angular's dependency injection.

Pros:

  1. Simplicity: It’s a simple, clean, and straightforward approach.
  2. Type Safety: Angular’s dependency injection system handles the service creation and injection, providing type safety.

Cons:

  1. Less Flexibility: This approach works well for a single instance of the service. However, if we want to use multiple instances of the service with different configurations in the same component, this approach falls short.
  2. Direct Coupling: The service is directly injected into the component, creating a tight coupling between the service and the component.

Approach 2: @Inject('postsService'):

This approach uses an InjectionToken to create and inject different instances of the service. In the providers array of the component, we use a factory function to create a new instance of the service with a specific configuration and provide it with a string token ('postsService' in this case). Then, we use the @Inject decorator with the corresponding token to inject the service.

Pros:

  1. Flexibility: This approach allows us to inject multiple instances of the service with different configurations in the same component.
  2. Looser Coupling: The use of string tokens for injection allows for a looser coupling between the service and the component.

Cons:

  1. Complexity: This approach is a bit more complex and requires more understanding of Angular’s dependency injection system.
  2. Possible Type Errors: As we use string tokens for injecting the service, Angular’s dependency injection system can’t help us catch type errors. We have to be careful to use the correct string token and to ensure that the service is correctly typed.

Both approaches have their strengths and weaknesses. Depending on your requirements, you might find one approach more suitable than the other. Remember, choosing the right approach depends on the specific needs of your application.

Advantages of Generic CRUD Service

  1. Code Reusability: The generic CRUD service allows us to write the core CRUD operations once and reuse them across various data models. This significantly reduces code duplication and keeps our codebase clean and maintainable.
  2. Type Safety: By leveraging TypeScript’s generic types, we maintain strong typing and type safety. This catches potential errors at compile time and enhances the overall reliability of our code.
  3. Simplified Maintenance: Since we’re dealing with a single generic service, any changes or improvements made to the CRUD operations will automatically apply to all data models that use the service, making maintenance more straightforward and efficient.
  4. Customizable Endpoints: By injecting a configuration object, each instance of the GenericService can use a different API endpoint, making it easy to work with multiple data models using a single service.

Conclusion

In this article, we’ve explored how to create and utilize the Generic CRUD Service (GenericService) in Angular using generic types and Angular’s Dependency Injection system. By leveraging TypeScript’s generics, we can build a versatile and type-safe service that streamlines CRUD operations for various data models. This approach promotes code reusability, reduces boilerplate code, and enhances the maintainability of Angular applications. Embrace the power of generic types and the flexibility of Angular’s Dependency Injection to take your application development to the next level.

--

--

A. Waris

Full Stack Engineer | GoLang | .NET Core | Java Spring boot | AWS | Azure | Node.js | Python | Angular | React 🚀✨