Enhancing Code Reusability with NestJS Abstract Repository Pattern Best Practices and Examples

Enhancing Code Reusability with NestJS Abstract Repository Pattern Best Practices and Examples

Repositories are classes or components that encapsulate the logic required to access data sources. They centralize common data access functionality, providing better maintainability and decoupling the infrastructure or technology used to access databases from the domain model layer. — Design the infrastructure persistence layer

The abstract repository pattern is a design approach commonly used in software development to separate the data access logic from the rest of the application. This pattern promotes modularity, maintainability, and testability of code. When applied to the NestJS framework, which is a popular Node.js framework for building scalable and maintainable applications, the abstract repository pattern can be a powerful tool.

In NestJS, the abstract repository pattern can be implemented using services and interfaces.

In this article, we will be going through the process of creating an abstract repository pattern using Postgres and TypeORM

First, we start by creating a Nestjs Project

Install the Nest CLI globally

npm install -g @nestjs/cli

Create a new Nest project

nest new nest_typeorm

Start up your Nestjs App on development mode by running npm run start:dev

In this article, we will be building a simple API, where users can create a new user, find All Users, find Users, and so on by ID using the abstract repository pattern, Typeorm, and Postgres

Now let's spin up our Postgres Database with Docker.

Create a docker-compose.yml file in your root directory, and also a .env file that would contain all environment variables to spin up our Postgres container.

.env

POSTGRES_PASSWORD=password
POSTGRES_USER=emeke
POSTGRES_DB=user

docker-compose.yml

version: '3'
services:
  postgres:
    image: postgres:latest
    restart: unless-stopped
    env_file:
      - ./.env
    ports:
      - "5434:5432"
    volumes:
      - /var/lib/postgresql/data

Spin up ur Postgres Database by running docker-compose up , Ensure Docker Deamon is running in the background.

Install typeorm , @nestjs/typeorm

npm i typeorm @nestjs/typeorm pg
import {Module} from '@nestjs/module'
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
   imports:[TypeOrm.forRootAsync({
       useFactory: () => {
      return {
        type: 'postgres',
        url: 'postgresql://emeke:password@localhost:5434/user',
        autoLoadEntities: true,
        synchronize: true,
        host: 'localhost',
        entities:[

        ]
      }
    }
})]
})

export class AppModule{}

Now let's start by building the user module

nest g mo user

nest g s user --no-spec

nest g co user --no-spec

The above commands would generate a user module, user controller, and user service in a user folder inside the src folder

user.module.ts

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  controllers: [UserController],
  providers: [UserService]
})
export class UserModule {}

user.controller.ts

import {Controller} from '@nestjs/common'

@Controller('user')
export class UserController{}

user.service.ts

import {Injectable} from '@nestjs/common'

@Injectable()
export class UserService{}

user.entity.ts

import { Entity, Column, OneToMany,PrimaryGenerateColumn} from "typeorm";

@Entity('user')
export class User  {
    @PrimaryGenerateColumn()
    id:string
    @Column({ nullable: false })
    firstname: string

    @Column({ nullable: false })
    lastname: string


    @Column({ unique: true, nullable: false })
    businessname: string

    @Column({ unique: true })
    email: string

    @Column()
    password: string

    @Column({ nullable: true })
    profilepicture: string

}

Now that we have our project running the first step to implementing the Abstract Repository pattern is to define Interfaces, These interfaces will serve as a contract that concrete repository implementations need to adhere to.

base.interfaces.ts

import { DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from "typeorm";

export interface BaseInterfaceRepository<T> {
    create(data: DeepPartial<T>): T;
    createMany(data: DeepPartial<T>[]): T[]
    save(data: DeepPartial<T>): Promise<T>
    saveMany(data: DeepPartial<T>[]): Promise<T[]>
    findOneById(id: string): Promise<T>
    findByCondition(filterCondition: FindOneOptions<T>): Promise<T>;
    findAll(options?: FindManyOptions<T>): Promise<T[]>
    remove(data: T): Promise<T>
    findWithRelations(relations: FindManyOptions<T>): Promise<T[]>
    preload(entityLike: DeepPartial<T>): Promise<T>
    findOne(options: FindOneOptions<T>): Promise<T>
}

The next step is to create our base repository, This class should handle the actual data access and manipulation using database operations.

base.repository.ts

import { DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere, Repository } from "typeorm";
import { BaseInterfaceRepository } from "./interface/base.interfaces";

interface HasId {
    id: string
}

export abstract class BaseAbstractRepostitory<T extends HasId> implements BaseInterfaceRepository<T>{
    private entity: Repository<T>
    protected constructor(entity: Repository<T>) {
        this.entity = entity
    }

    public async save(data: DeepPartial<T>): Promise<T> {
        return await this.entity.save(data)
    }

    public async saveMany(data: DeepPartial<T>[]): Promise<T[]> {
        return this.entity.save(data)
    }

    public create(data: DeepPartial<T>): T {
        return this.entity.create(data)
    }

    public createMany(data: DeepPartial<T>[]): T[] {
        return this.entity.create(data);
    }

    public async findOneById(id: any): Promise<T> {
        const options: FindOptionsWhere<T> = {
            id: id
        }
        return await this.entity.findOneBy(options)
    }

    public async findByCondition(filterCondition: FindOneOptions<T>): Promise<T> {
        return await this.entity.findOne(filterCondition)
    }

    public async findWithRelations(relations: FindManyOptions<T>): Promise<T[]> {
        return await this.entity.find(relations)
    }

    public async findAll(options?: FindManyOptions<T>): Promise<T[]> {
        return await this.entity.find(options)
    }

    public async remove(data: T): Promise<T> {
        return await this.entity.remove(data)
    }

    public async preload(entityLike: DeepPartial<T>): Promise<T> {
        return await this.entity.preload(entityLike)
    }

    public async findOne(options: FindOneOptions<T>): Promise<T> {
        return this.entity.findOne(options)
    }
}

Let's create a user interface that extends the BaseInterfaceRepository

user.interface.ts

import { BaseInterfaceRepository } from "../common/core/interface/base.interface.repository";
import { User } from "./entities/user.entity";

export interface UserRepositoryInterface extends BaseInterfaceRepository<User> {}

Now let's make use of the base repository we will be extending the base repository class and implementing the base interface.

user-repository.ts

import { InjectRepository } from "@nestjs/typeorm";
import { BaseAbstractRepostitory } from "../common/core/repository/base.repository";
import { CInterface } from "./user.interface";
import { User } from "./entities/user.entity";
import { Repository } from "typeorm";


export class UserRepository extends BaseAbstractRepostitory<User> implements UserRepositoryInterface {
    constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) {
        super(userRepository)
    }
}

After creating a User Repository you can then import it across different services. By extending the BaseAbstractRepository Class the UserRepository has access to all the function calls built into the BaseAbstactRepository, this reduces the complexity of the code and makes the code reusable.

By using the abstract repository pattern in NestJS, you achieve a clear separation of concerns between the data access layer and the business logic layer. This enhances the maintainability and testability of your application, allowing you to switch out underlying data storage implementations with minimal impact on the rest of the codebase.

An Abstract Repository pattern is a great way to decouple your app from external DB frameworks, and generic repositories reduce the amount of code you need to write in order to accomplish it.

Check Github for Full Code