Today, let's dive into a crucial aspect of software development: writing cleaner, more maintainable code.
One of the most effective ways to achieve this is by leveraging the power of TypeScript in our NestJS applications.
After creating your NestJS app, you'll often find yourself generating resources for your application, such as:
nest g res test
This command generates a new resource, such as a controller, service, and module. Initially, this approach works well.
However, as your application grows and you start creating more resources, you'll notice a common pattern: code repetition.
The Problem: Code Duplication
When building out your application, it’s easy to find yourself repeating the same or similar code over and over again. Whether it’s in controllers, services, or repositories, this repetitive code can quickly become hard to maintain, test, and extend.
The Solution: Embrace DRY
This is where the DRY (Don’t Repeat Yourself) principle comes into play. The DRY principle encourages us to eliminate repetitive code, making our applications easier to maintain, more efficient, and scalable. So, how can we avoid this performance and maintainability issue?
Refactoring for DRY
To demonstrate, let's take a closer look at some common ways to refactor and improve code using TypeScript’s features:
Create Reusable Services:
Instead of duplicating logic in every controller, create a an abstract controller and service with reusable methods that handle common operations like data validation, error handling, or database queries.
Use Generics:
TypeScript's generics can be an excellent way to reduce redundancy. By defining generic functions or classes, you can handle different data types without repeating the same logic for each type.
Utilize Inheritance and Interfaces:
In object-oriented programming (OOP), inheritance allows you to abstract common logic into base classes. Similarly, interfaces can ensure that different components adhere to the same structure without duplicating logic.
Create Shared Modules:
NestJS allows you to bundle related services, controllers, and providers into shared modules, so you don't have to replicate them across your app. This approach promotes better organization and code reuse.
Let’s dive into how you can refactor both the controller and service layers in your NestJS application to avoid code duplication and improve maintainability. We'll also show how to leverage TypeORM's built-in functionality by creating a reusable CrudService.
Step 1: The Test Component and Service Before Refactor
Before we refactor, let’s take a look at the original code for the TestController and TestService:
In this implementation, the TestService is defining all the CRUD operations manually. While this works fine for small applications, it becomes inefficient as the project grows and the same logic gets repeated in every service.
Step 2: Introducing the CrudService Class
To refactor and optimize, we can introduce a generic CrudService that contains common CRUD operations for TypeORM entities. The CrudService will use TypeORM’s built-in logic for database operations, which will make it easier to handle CRUD tasks.
Here’s the CrudService that we will use in our refactored service:
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
@Injectable()
export abstract class CrudService<T> {
constructor(private repository: Repository<T>) {}
async save(entity) {
return await this.repository.save(entity);
}
async findAll() {
return await this.repository.find({});
}
async remove(id: string) {
return await this.repository.delete(id);
}
}
This CrudService is an abstract class that can be extended by any service that needs to handle CRUD operations for TypeORM entities. It includes methods to save, findAll, and remove entities, all powered by TypeORM’s Repository.
Step 3: Refactoring the TestService
Now, let’s refactor the TestService to extend CrudService and utilize TypeORM’s logic. By doing this, we eliminate the need to manually write CRUD operations in every service, and instead, reuse the logic provided by CrudService.
Here’s the updated TestService:
import { Injectable } from '@nestjs/common';
import { CrudService } from 'src/utils/crud.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TestEntity } from './entities/test.entity';
@Injectable()
export class TestService extends CrudService<TestEntity>{
constructor(
@InjectRepository(TestEntity)
private testEntityRepository: Repository<TestEntity>,
) {
super(testEntityRepository)
}
}
Notice how the TestService now extends the CrudService and uses the TestEntity repository. The service no longer needs to define the create, findAll, remove, and other CRUD methods. These are inherited from the CrudService, allowing us to focus on the specific logic of our TestEntity.
Step 4: Refactoring the TestController
Now, let’s look at the refactored TestController. Since the TestService already handles the CRUD operations, we can simplify the controller:
First create our BaseController like below:
import { Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
export abstract class BaseController<T, CreateDto, UpdateDto> {
constructor(protected readonly service: T) {}
@Post()
create(@Body() createDto: CreateDto) {
return (this.service as any).create(createDto);
}
@Get()
findAll() {
return (this.service as any).findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return (this.service as any).findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateDto: UpdateDto) {
return (this.service as any).update(+id, updateDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return (this.service as any).remove(+id);
}
}
Then update TestController to be as below:
import { Controller } from '@nestjs/common';
import { TestService } from './test.service';
import { CreateTestDto } from './dto/create-test.dto';
import { UpdateTestDto } from './dto/update-test.dto';
import { BaseController } from './abstract.controller';
@Controller('test')
export class TestController extends BaseController<TestService, CreateTestDto, UpdateTestDto> {
constructor(private readonly testService: TestService) {
super(testService);
}
}
Benefits of This Approach
By introducing the CrudService and refactoring the TestService and TestController, we achieve the following:
Simplified Layer: We no longer need to write repetitive CRUD operations. The BaseController and CrudService handles all basic logic.
Reduced Code Duplication: Common QUERIES and CRUD logic is centralized in one place, making the codebase cleaner and easier to maintain.
TypeORM Integration: The CrudService leverages TypeORM’s repository pattern, so all database operations are handled efficiently and consistently.
Easier to Scale: When adding new entities, you can simply create a new service that extends the CrudService and inject the corresponding repository.
Conclusion
By refactoring the TestService and TestController to utilize the BaseController and CrudService, we streamline our code and follow best practices like DRY (Don’t Repeat Yourself).
This approach not only simplifies the code but also makes the application more scalable and maintainable.
Top comments (0)