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.