Logo
Published on

Mastering Data Management: Exploring the Repository Pattern in NestJS

Authors
  • Name
    Twitter

Introduction

In today’s fast-paced world of software development, efficient data management is a cornerstone of success. NestJS, a popular Node.js framework, offers a powerful and flexible approach to building APIs and services. However, to truly harness the potential of NestJS and ensure clean, maintainable code, you’ll want to add a key ingredient to your toolkit: the Repository Pattern.

In this article, we’ll explore the transformative benefits of incorporating the Repository Pattern into your NestJS projects. You’ll discover how this pattern streamlines your data access layer, simplifies your codebase, and ultimately makes your software more robust and scalable.

What is repository pattern ?

“The Repository Pattern, being a software design pattern, separates data access logic from the application, providing a streamlined API for data interactions and concealing data storage intricacies. Implementing this pattern enhances code organization, facilitates testing, and detaches your application from specific data storage technologies”

Prerequisites

Before diving into the implementation of the Repository Pattern in NestJS, it’s essential to ensure you have a foundational understanding of the following prerequisites:

  1. Node.js and NestJS Basics

  2. JavaScript/TypeScript Knowledge

  3. RESTful API Knowledge

  4. Database Basics

  5. NestJS Project Setup

Let’s start by creating a user schema in NestJS using the Mongoose ORM.

@Schema({ collection: "users", timestamps: true })
export class User implements IUser {
  @Prop({ type: String, required: true })
  firstName: string;

  @Prop({ type: String, required: true })
  lastName: string;

  @Prop({ type: String, required: true })
  email: string;

  @Prop({ type: String, required: true })
  password: string;

  @Prop({ type: String, required: true })
  role: ROLE;
}

export const UserSchema = SchemaFactory.createForClass(User);

The next imperative step is importing our schema, and this action holds significant value.

@Module({
  imports: [
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
  ],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

“Let’s create a base repository that the user repository will extend for cleaner and more reusable code.”

export class BaseRepository<T> implements IBaseRepository<T> {
  repository: Model<T>;

  constructor(repository: Model<T, Record<string, any>, Record<string, any>>) {
    this.repository = repository;
  }

  async create(payload: Partial<T>): Promise<T> {
    return this.repository.create(payload);
  }

  async find(query?: IQueryFilter<T> | any): Promise<T[]> {
    const { offset, limit, order, ...filter } = query;
    console.log(offset, limit, order);

    return await this.repository.find(filter);
  }

  async findOne(query: IQueryFilter<T>): Promise<T> {
    return await this.repository.findOne(query);
  }

  async updateOne(
    query: Record<string, unknown>,
    payload: Partial<T>,
    projection?
  ): Promise<unknown> {
    return await this.repository.updateOne(query, payload, projection);
  }

  async deleteOne(query?: Record<string, unknown>): Promise<unknown> {
    return await this.repository.deleteOne(query);
  }
}

Now, we can proceed to create our user repository by extending our base repository.

export class UserRepository extends BaseRepository<UserDocument> {
  constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {
    super(userModel);
  }
}

Now, we can move forward with implementing this in our user service. By doing this, we abstract the user schema, leveraging the user repository for our database operations. This demonstrates the advantageous use of a repository pattern in software development, fostering better code organization, testability, and reduced coupling with the database layer.

Introducing our base service, which the user service extends for added functionality.

export class BaseService<R, Q, C, U> {
  private repository: R | any;
  private name: string;

  constructor(repository: R, name: string) {
    this.repository = repository;
    this.name = name;
  }

  async create(payload: C): Promise<{ data: any; message: string }> {
    const data = await this.repository.create(payload);

    return { data, message: `${this.name} successfully created` };
  }

  async find(query?: Q): Promise<{ data: any; message: string }> {
    const data = await this.repository.find(query);
    if (!data || data.length < 1)
      throw new NotFoundException(`${this.name}s not found`);

    return { data, message: `${this.name} successfully fetched` };
  }

  async update(
    id: string,
    payload: U
  ): Promise<{ data: any; message: string }> {
    const mongoId = new ObjectId(id);

    const data = await this.repository.updateById(mongoId, payload);
    if (!data) throw new NotFoundException(`${this.name} not found`);

    return { data, message: `${this.name} successfully updated` };
  }

  async delete(id: string): Promise<unknown> {
    const mongoId = new ObjectId(id);

    await this.repository.deleteById(mongoId);

    return { data: null, message: `${this.name} successfully updated` };
  }
}

Finally, our user service,

export class UserService extends BaseService<
  UserRepository,
  QueryUserDto,
  CreateUserDto,
  UpdateUserDto
> {
  constructor(private readonly userRepository: UserRepository) {
    super(userRepository, "user");
  }

  async findAllAndSearch(
    query: QueryUserDto
  ): Promise<{ data: any; message: string }> {
    const searchFields = ["email", "firstName", "lastName"];
    const filter = query.search
      ? regexSearchQuery(searchFields, query.search, query)
      : query;

    const users = await this.userRepository.paginate(filter);
    if (!users || users.length < 1)
      throw new NotFoundException(`users not found`);

    return { data: users, message: `users successfully fetched` };
  }
}

With the Repository Pattern now integrated into our user service, we’ve improved code organization, maintainability, and data management flexibility. Next, let’s proceed to utilize our user service in the user controller.

Conclusion

Incorporating the Repository Pattern in NestJS offers a clear path to cleaner, more maintainable, and scalable applications. With its ability to abstract data access, enhance testability, and reduce dependencies on specific data storage technologies, this pattern is a vital tool in any developer’s toolkit. Embrace it and unlock the full potential of your NestJS projects.