page-cover
page-icon
NestJS
NestJS Boilerplate using Typescript Generics.
Brahim Abdelli-avatarBrahim Abdelli2023-06-09Generics

The Context 🔗

This article will detail the creation of a NestJS Boilerplate, if you are not interested in seeing how this project was developed, then you can skip to the sections that you like or you can jump directly to the GitHub project.

What is a boilerplate? 🔗

It is a reusable set of code that can be implemented over and over again in different projects.
This boilerplate aims to help you write code quickly and get a lot, hence gaining time and effort while developing your future projects, this article will walk you through the different technologies and stacks used while developing this NestJS boilerplate.
This project uses MongoDB and TypeORM.

What does this boilerplate provide? 🔗

This boilerplate provides:
  • Base Controller, Service, Entity, and DTOS that you can use for your modules.
  • A tested two modules, a category module that fully implements the base logic and a product module that partially implements the base logic, and overrides a method from the controller and service.
  • Ready to use APIs :
    • findAll
    • paginate
    • findOne
    • create
    • update
    • delete
    • logical deletion (by boolean value)
    • clear
    • search
  • An authentification module and middleware.
  • A mailer service using (MailJet) integrated with a handlebar .hbs template.
  • Pipes such as Abstract validation pipe for objects, ObjectID validation pipe, etc.
  • Utils for finding by field, checking field uniqueness, custom error throwing, etc.
  • The entirety of the functions and APIs mock and e2e tested.
  • Dev and prod environments.
  • Dockerized app.
You can access the GitHub repository to get the full project below and entirely for free.
Let’s get started!

Notice 🔗

So we’re going to break down the boilerplate chunk by chunk.
I decided to divide this section into two sections:
The first one is the abstraction part, where we will be talking about the base logic (entity, service, and controller), and the second section will contain the implementation of the abstraction module and the development of modules on top of it.

Section 1: Abstraction 🔗

The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise. Edsger Dijkstra
Started with some words of wisdom, trying to capture the spirit of abstraction and the purpose of creating an abstraction layer.
Let’s say that we are creating a new project that will hold 10+ modules, for each exists a controller, repository, entities, DTOs, etc, for those modules we will have similar methods with some variations for each in our service, and in our controller, a lot of similar properties within the entities and the dtos too, this will be adding a lot of repetition to our code, our bugs will be decentralized.
Found it on the internet when typing “abstraction technology”, looks fancy and great !
Found it on the internet when typing “abstraction technology”, looks fancy and great !
Though the confusingly nice picture above aims to encapsulate abstraction in a graphical art form, I think that it is the path to clarity.
So using a base controller and service can provide several benefits in a software project. Here are some reasons why you may choose to use it:
  1. Code Reusability: A base controller and service allow you to define common functionality and operations that can be reused across multiple modules or entities in your application. Instead of duplicating code for similar operations, you can centralize the common logic in the base controller and service and have other modules or entities inherit from them.
  1. DRY Principle (Don't Repeat Yourself): By encapsulating common functionality in a base controller and service, you can avoid duplicating code throughout your application. This leads to cleaner, more maintainable code that is easier to understand and modify.
  1. Standardization: A base controller and service provide a standardized structure and set of operations for working with entities in your application. By defining common CRUD (Create, Read, Update, Delete) operations or other common functionality in the base classes, you ensure consistency and maintain a clear code structure across your application.
  1. Rapid Development: With a base controller and service in place, you can accelerate the development process. By inheriting from these base classes, you inherit the common functionality and operations, allowing you to write less code and focus on the specific requirements of your modules or entities. This can help to reduce development time and increase productivity.
  1. Separation of Concerns: A base controller and service can help in separating concerns and maintaining a modular architecture. The base controller can handle HTTP request handling and routing, while the base service can manage business logic and database operations. This separation allows for better organization and makes the codebase more manageable and scalable.
  1. Easy Maintenance and Updates: Having common functionality in a base controller and service simplifies maintenance and updates. If changes or improvements are required, you can make them in the base classes, and the changes will be inherited by all the modules or entities that extend them. This reduces the effort required to update and maintain the application.
Overall, using a base controller and service promotes code reusability, standardization, and separation of concerns. It helps in achieving a cleaner, more maintainable codebase and allows for rapid development by reducing code duplication. It also simplifies maintenance and updates, making the application more scalable and easier to work with.
So let’s jump straight into it.
Let’s start by explaining the base entity.
  • It is an abstract class that serves as the base for other entities in the application.
  • It includes properties and methods that can be reused by extending classes.
typescript
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Transform } from 'class-transformer';
import { BeforeInsert, BeforeUpdate, Column, ObjectID, ObjectIdColumn } from 'typeorm';
import { IUser } from '../../modules/users/interface/user.interface';
import { transformEntity } from '../utils/transform-entity.utlis';

export abstract class BaseEntity {
  @ApiProperty()
  @ObjectIdColumn()
  @Transform(transformEntity)
  @Expose()
  public _id: ObjectID;

  @Column()
  @Expose()
  public isDeleted: boolean;

  @Column()
  @Transform(transformEntity)
  public userCreated: ObjectID | IUser;

  @Column()
  @Expose()
  protected createdAt: Date;

  @Column()
  @Transform(transformEntity)
  public userUpdated: ObjectID | IUser;

  @Column()
  @Expose()
  protected lastUpdateAt: Date;

  /**************** ACTIONS ****************/

  @BeforeInsert()
  @BeforeUpdate()
  private beforeActions() {
    this.lastUpdateAt = new Date();
  }

  @BeforeInsert()
  private beforeInsertActions() {
    this.createdAt = new Date();
  }
}
For this project, I assumed that all these properties declared, will be used in every entity that extends from this one,
  • The beforeActions() and beforeInsertActions() methods are private methods defined within the BaseEntity class, They are annotated with lifecycle hook decorators and perform actions such as updating timestamps (lastUpdateAt, createdAt).
  • @ApiProperty(): Decorator from the Swagger module used to define a property for API documentation.
  • @ObjectIdColumn(): Decorator from the TypeORM module used to mark the _id property as the primary identifier.
  • @Transform(transformEntity): Decorator from the class-transformer module used to apply transformation logic during serialization/deserialization.
  • @Expose(): Decorator from the class-transformer module used to indicate that a property should be exposed during serialization.
The same logic goes for the create and update DTOs, you can add and delete whatever properties you like to them.
throw-error.utils.ts:
This method throws an exception :
typescript
import { HttpException } from '@nestjs/common/exceptions/http.exception';

export function throwError(errors, message, code = 400) {
  throw new HttpException(
    {
      message,
      errors
    },
    code
  );
}
Simply a custom function that throws an HTTPException, more customized than the one provided by NestJS, below is an example of its implementation.
typescript
throwError({ User: 'Not found' }, 'Authentication failed', 401);
is-field-unique.utils
Returns a boolean representing the uniqueness of a field, you can check the code to comprehend more the function logic
typescript
import { ObjectLiteral, Repository } from 'typeorm';
import { throwError } from './throw-error.utils';

/**
 * Check if the given field is unique
 * @param repository  corresponding entity repository
 * @param field object containing a single field
 * @example {_id : "645ead8b586d13a6932d46dd" }
 * {title : "some title to check if exists"}
=
 * @returns boolean representing the uniqueness of the field
 */
export async function isFieldUnique<T>(repository: Repository<T>, field: object, id?: string): Promise<boolean> {
  // get name and value from object field
  const fieldKey: string = Object.keys(field)[0];
  const fieldValue: any = Object.values(field)[0];

  const condition = { [fieldKey]: new RegExp(`^${fieldValue}$`, 'i') };
  const entity: ObjectLiteral = await repository.findOne({ where: condition });

  let isUnique = false;

  if (id) {
    if (!entity) isUnique = true;
    else isUnique = entity._id.toHexString() === id && entity[fieldKey].toLowerCase() === fieldValue.toLowerCase();
  } else isUnique = !!!entity; // false if entity is not found

  if (!isUnique) throwError({ [`${fieldKey}isUnique`]: fieldKey + ' must be unique.' }, 'Input data validation failed');

  return isUnique;
}
Below is the implementation :
typescript
await isFieldUnique(this.userRepository, { username: userData.username }, params.id);
find-by-field.utils.ts
The findByField function provides a convenient way to search for an entity in a repository based on a specific field and value. It handles different scenarios, including searching by the entity's identifier or a regular field, and provides the option to throw an error if the entity is not found.
typescript
import { ObjectID } from 'mongodb';
import { Repository } from 'typeorm';
import { throwError } from './throw-error.utils';
/**
 * Find entity with matched field
 * @param repository  corresponding entity repository
 * @param field object containing single field
 * @example {_id : "645ead8b586d13a6932d46dd" }
 * {title : "some title to check if exisits"}
 * @param omitError whether to throw error if entity not found
 * @default false
 * @param insensitive if string check is case insensitive
 * @default true
 * @returns the entity found else null
 */
export async function findByField<T>(
  repository: Repository<T>,
  field: object,
  omitError: boolean = false,
  insensitive: boolean = true
): Promise<T> {
  // get name and value from object field
  const fieldKey: string = Object.keys(field)[0];
  const fieldValue: any = Object.values(field)[0];

  let entity: T = null;

  if (!fieldKey || !fieldValue) return entity;

  if (['_id', 'id'].includes(fieldKey)) {
    const id = new ObjectID(fieldValue);
    entity = await repository.findOne(id.toHexString());
  } else {
    const condition = { [fieldKey]: new RegExp(`^${fieldValue}$`, insensitive ? 'i' : undefined) };
    entity = await repository.findOne({ where: condition });
  }

  if (omitError === true && !entity)
    throwError(
      { [repository.metadata.tableName]: 'Not found' },
      'Entity not found check the ' + fieldKey.replace('_', ''),
      404
    );

  return entity;
}
findByField<T>(...): The function is generic and can accept a type parameter T.
Parameters:
  • repository: Repository<T>: The repository parameter represents a TypeORM repository for a specific entity type T.
  • field: object: The field parameter is an object that represents the field and value to search for in the entity.
  • omitError: boolean = false: The omitError parameter is a boolean flag indicating whether to throw an error if the entity is not found. It is set to false by default.
  • insensitive: boolean = true: The insensitive parameter is a boolean flag indicating whether the search should be case-insensitive. It is set to true by default.
This is an implementation of the findByField function :
typescript
const user = await findByField(this.userRepository, { email: params.email }, true);
transform-entity.utlis.t
Transforms value to ObjectID.
typescript
import { ObjectID } from 'mongodb';
/**
 * To use in @Transform decorator to transform property value
 * @param value ObjectID or entity Object to transform
 */
export function transformEntity({ value }) {
  if (value?._id) value._id = new ObjectID(value._id).toHexString();
  // the value it's a entity object
  else if (value) value = new ObjectID(value).toHexString(); // the value it's just an id
  return value; // return the same passed value in case it's false
}
This is how we will be using this function (inside an entity):
typescript
	@ApiProperty()
  @ObjectIdColumn()
  @Transform(transformEntity)
  @Expose()
  public _id: ObjectID;
validate-object-id.pipe.ts
typescript
import { Injectable, PipeTransform } from '@nestjs/common';
import { ObjectID } from 'mongodb';
import { throwError } from '../utils/throw-error.utils';

@Injectable()
export class ValidateObjectIdPipe implements PipeTransform<any> {
  constructor(private readonly entity: any) {}

  async transform(params: any) {
    if (!params?.id) throwError({ [this.entity ? this.entity : `${'Entity'}`]: 'Not found' }, 'No ID provided');

    if (!ObjectID.isValid(params.id))
      throwError({ [this.entity ? this.entity : `${'Entity'}`]: 'Not found' }, 'Check passed ID');
    return params;
  }
}
Adding the ValidateObjectId.pipe.ts to verify if the ID passed matched an _id stored in the database or not, if not, it throws an error.
abstract-validation.pipe.ts
typescript
import { ArgumentMetadata, Injectable, Type, ValidationPipe, ValidationPipeOptions } from '@nestjs/common';

@Injectable()
export class AbstractValidationPipe extends ValidationPipe {
  constructor(
    options: ValidationPipeOptions,
    private readonly targetTypes: { body?: Type; query?: Type; param?: Type }
  ) {
    super(options);
  }

  async transform(value: any, metadata: ArgumentMetadata) {
    const targetType = this.targetTypes[metadata.type];
    if (!targetType) {
      return super.transform(value, metadata);
    }
    return super.transform(value, { ...metadata, metatype: targetType });
  }
}
Here's an explanation of the class :
  • The AbstractValidationPipe extends the ValidationPipe class and overrides the transform method to provide custom transformation and validation behavior.
  • The class constructor accepts two parameters: options of type ValidationPipeOptions and targetTypes of type { body?: Type; query?: Type; param?: Type }.
  • The options parameter is used to configure the behavior of the underlying ValidationPipe class.
  • The targetTypes parameter is an object that specifies the target type for each metadata type (body, query, param).
Below is an implementation, and let me explain it furthermore :
typescript
const createPipe = new AbstractValidationPipe({ whitelist: true, transform: true }, { body: createDto });
  • It’s an initialization of a new instance of the AbstractValidationPipe class and assigns it to the createPipe variable.
  • The AbstractValidationPipe constructor is called with two arguments:
    • The first argument { whitelist: true, transform: true } is an object specifying the options for the underlying ValidationPipe. It enables both whitelisting and transformation during validation.
    • The second argument { body: createDto } is an object specifying the target type for the body metadata type. It indicates that the target type for the body should be createDto.
To recapitulate, the AbstractValidationPipe class extends the ValidationPipe class and provides custom transformation and validation behavior. Later we create an instance of the AbstractValidationPipe class with specific options and target types, specifically indicating that the target type for the body metadata type should be createDto. This allows for validation and transformation of the create route handler's request body using the specified DTO (createDto).
Now that we laid out our entity, DTOs, utils, and pipes, time to dive into the controller.
When I write APIs, I always like to think from the controller point of view, and then back to the service, the repo, etc.., it’s a reflex I developed from writing frontend applications.
Let’s start by explaining the foundation of the base controller
typescript
import { Type } from '@nestjs/common';
import { AbstractValidationPipe } from '../pipes';
import { BaseEntity } from './base.entity';
import { IBaseController } from './interfaces/base-controller.interface';
import { IBaseService } from './interfaces/base-service.interface';

export function BaseController<T extends BaseEntity, createDto, updateDto>(
  createDto: Type<createDto>,
  updateDto: Type<updateDto>
): Type<IBaseController<T, createDto, updateDto>> {
  const createPipe = new AbstractValidationPipe({ whitelist: true, transform: true }, { body: createDto });
  const updatePipe = new AbstractValidationPipe({ whitelist: true, transform: true }, { body: updateDto });

  class GenericsController<T extends BaseEntity, createDto, updateDto>
    implements IBaseController<T, createDto, updateDto>
  {
    constructor(private readonly service: IBaseService<T, createDto, updateDto>) {}
  }
  return GenericsController;
}
By using the BaseController function, you can quickly generate a generic controller class for a specific entity.
Let’s start by explaining the code bit by bit:
  • The BaseController function is a higher-order function that returns a dynamically generated class.
  • The GenericsController class is defined, implementing the IBaseController interface with the provided type parameters.
  • It takes two type parameters (createDto and updateDto) that represent the DTOs for create and update operations.
  • IBaseController and IBaseService are imported from the interfaces directory. They represent interfaces for the controller and service.
  • AbstractValidationPipe and ValidateObjectIdPipe are custom pipes used for validation and transformation.
And then we proceed to write the APIs like this one :
typescript
@Get()
async findAll(): Promise<T[]> {
  return this.service.findAll();
}
Let’s start by explaining the core of the base service, it is an abstract class that provides a base implementation for service classes. It takes in a repository of type Repository<T> and user authentication information from the request. This allows service classes that extend BaseService to have access to the repository for performing database operations on the entity type T and access to the user authentication information, below is the base of the baseService (no pun intended) :
typescript
export abstract class BaseService<
  T extends BaseEntity,
  createDto extends BaseCreateDto,
  updateDto extends BaseUpdateDto
> implements IBaseService<T, createDto, updateDto>
{
  constructor(
    private readonly repository: Repository<T>,
    @Inject(REQUEST) public readonly request: IGetUserAuthInfoRequest
  ) {}
}
As you know, the BaseService class is an abstract class that serves as the base for other service classes.
  • It is parameterized with three generic types: T, createDto, and updateDto.
  • T represents the entity type that the service will work with, and createDto and updateDto represent the DTO types used for creating and updating entities, respectively.
  • The class implements the IBaseService interface, which defines the contract for base service functionality, which is nothing but a collection of the methods
The constructor now has two parameters :
  • The first parameter is a repository, of type Repository<T>, which represents the repository used for database operations on the entity type T.
  • The second parameter is request, of type IGetUserAuthInfoRequest, which represents the user authentication information retrieved from the request.
  • The @Inject(REQUEST) decorator is used to inject the user authentication information from the request into the request parameter.
There are two parts of methods written inside the base service, ones are to be distributed to our APIs in the controller, and local ones that we will be using for our service methods.
These methods are only going to be used in the service class and are not going to be directly linked to our APIs.
populate
typescript
/**
   * Populate entit(y|ies) passed in paramter with "userCreated" and "userUpdated" properties
   * @param entities Array of entities OR an entity to populate
   * @returns Entit(y|ies) populated
   */
  private async populate(entities: Array<T> | any): Promise<Array<T> | any> {
    if (!entities) return;

    let tmp = entities;
    if (!Array.isArray(tmp)) tmp = [tmp];

    for (const idx in tmp) {
      const { userCreated, userUpdated } = tmp[idx];

      if (userCreated)
        tmp[idx].userCreated = await findByField(this.repository, { _id: userCreated?._id ?? userCreated });

      if (userUpdated)
        tmp[idx].userUpdated = await findByField(this.repository, { _id: userUpdated?._id ?? userUpdated });
    }

    return Array.isArray(entities) ? tmp : tmp[0];
  }
the populate method in the service class is used to populate the userCreated and userUpdated properties of the entities passed as the parameter. It iterates over the entities, checks for the existence of these properties, and calls the findByField function to retrieve the corresponding user entities. It then updates the userCreated and userUpdated properties of each entity with the retrieved user entities. The method returns the populated entities, either as an array or a single entity, based on the input.
createObject
typescript
private createObject(dto: createDto | updateDto): T {
    const newEntity = {} as T;
    return Object.assign(newEntity, dto);
  }
The createObject method is used to create a new instance of an entity (T) based on the provided DTO (dto). It creates an empty object of type T and copies the properties from the dto object to the newEntity object using Object.assign(). The method returns the newEntity object, which represents the created instance of the entity, i used it multiple times in the service, and could be used furthermore, that’s why I separated it into a method.
These methods are directly linked to the APIs in our controller,
findAll
typescript
async findAll(condition = { isDeleted: false }): Promise<T[]> {
    const where: FindConditions<T> = { ...condition };
    return this.repository.find({ where });
  }
The findAll method is responsible for retrieving multiple entities from the database based on a given condition, by default it’s { isDeleted: false }indicating that only non-deleted entities should be returned.
paginate
typescript
async paginate(take, skip, condition = { isDeleted: false }, type?): Promise<any> {
    const queryTake = Number(take) || PaginationConstants.DEFAULT_TAKE;
    const querySkip = Number(skip) || PaginationConstants.DEFAULT_SKIP;

    let where = { ...condition };
    if (type) {
      where = { ...condition, ...{ type } };
    }
    const [result, total] = await this.repository.findAndCount({
      where,
      //order: { createdAt: -1 },
      take: queryTake,
      skip: querySkip
    });
    return {
      data: result,
      count: total
    };
  }
The paginate method in the service class retrieves paginated entities from the database based on the specified condition, pagination parameters (take (The number of entities to retrieve per page) and skip (The number of entities to skip (offset) before retrieving the page)), and optionally a type parameter. It uses the findAndCount method to perform the search and returns a Promise that resolves to an object containing the retrieved entities and the total count of entities.
findOne
typescript
async findOne(_id: ObjectID): Promise<T> {
    // throws error 404 if not found
    const entity = await findByField(this.repository, { id: _id }, true);
    return await this.populate(entity);
  }
The findOne method in the service class retrieves a single entity from the database based on the provided _id parameter. It uses the findByField function to perform the search and throws an error if the entity is not found(Optional). The retrieved entity is then passed to the populate method, and the result is returned as a Promise that resolves to an entity of type T.
create
typescript
/**
   *
   * @param data : the CreateDTO of the submitted entity
   * @returns : The created entity
   */
  async create(data: createDto): Promise<T> {
    const newEntity = this.createObject(data);
    newEntity.isDeleted = false;
    if (this.request.user) {
      newEntity.userCreated = this.request.user._id;
      newEntity.userUpdated = this.request.user._id;
    }
    const entity = this.repository.create(newEntity as any);
    return this.repository.save(entity as any);
  }
The create method in the service class creates a new entity based on the provided data. It sets the necessary properties such as isDeleted, userCreated, and userUpdated before saving the entity to the database through the repository savemethod. The method returns a Promise that resolves to the created entity of type T.
update
typescript
/**
 *
 * @param _id : the ID of the entity
 * @param dto : the DTO to be assigned for the entity
 * @returns : The modified entity
 */
async update(_id: ObjectID, dto: updateDto): Promise<T> {
  let newEntity = this.createObject(dto);
  if (this.request.user) {
    newEntity.userUpdated = this.request.user._id;
  }
  newEntity = await this.repository.preload({
    _id: (await findByField(this.repository, { id: _id }, true))._id,
    ...dto
  } as any);
  return this.repository.save(newEntity as any);
  }
The update method in the service class updates an existing entity based on the provided _id and dto. It retrieves the existing entity, merges it with the updated properties from the dto using the preload method, assigns the necessary properties such as userUpdated, and saves the modified entity to the database using the repository save method. The method returns a Promise that resolves to the modified entity of type T.
updateStatus
typescript
/**
   *
   * @param _id : ObjectID of the given entity
   * This method applies logical deletion or restoration from the database by setting the isDeleted to true or false
   */
  async updateStatus(_id: ObjectID, isDeleted: boolean): Promise<T> {
    let entity = {} as T;
    entity = await findByField(this.repository, { _id }, true);
    entity.isDeleted = isDeleted;
    if (this.request.user) {
      entity.userUpdated = this.request.user._id;
    }
    return await this.repository.save(entity as any);
  }
The updateStatus method in the service class applies logical deletion or restoration to an entity by updating the isDeleted property based on the provided _id and isDeleted parameters. It retrieves the entity, updates the necessary properties such as isDeleted and userUpdated, and saves the updated entity to the database using the repository save method. The method returns a Promise that resolves to the updated entity of type T.
Its use differs depending on the controller API, here we are deleting :
typescript
@Patch('archive/:id')
async archive(@Param(new ValidateObjectIdPipe('')) params): Promise<T> {
  return this.service.updateStatus(new ObjectID(params.id), true);
}
And here we are restoring :
typescript
@Patch('unarchive/:id')
async unarchive(@Param(new ValidateObjectIdPipe('')) params): Promise<T> {
  return this.service.updateStatus(new ObjectID(params.id), false);
} 
delete
typescript
async delete(_id: ObjectID): Promise<void> {
    await findByField(this.repository, { _id }, true);
    await this.repository.delete(_id);
}
In summary, the delete method in the service class deletes an entity from the database based on the provided _id. It ensures that the entity exists, performs the deletion using the repository delete method, and returns a Promise that resolves to void.
clear
typescript
/**
   * This method deletes permanently from the database
   */
  async clear(): Promise<void> {
    try {
      await this.repository.clear();
    } catch (error) {
      if (error.name === 'MongoError' && error.code === 26) {
        // Handle "is not found" error
        // Perform alternative logic or error handling
        console.log('Collection does not exist. Unable to clear.');
      } else {
        // Handle other errors
        console.log('An error occurred:', error);
      }
    }
  }
This method clears the table, in case the table exists.
search
This is the fun part, loved writing this method, so this method is designed to write to return a SearchResponseobject after querying from the database.
typescript
async search(data: QueryDto<T>): Promise<SearchResponse<T>> {
    const query: FindManyOptions<T> = { where: {} }; // initialize query to an empty object

    const queryTake = +data.take || PaginationConstants.DEFAULT_TAKE;
    const querySkip = +data.skip || PaginationConstants.DEFAULT_SKIP;
    const filterCriteria = data.attributes.map(attribute => {
      return {
        [attribute.key]:
          attribute.comparator == ComparatorEnum.EQUALS
            ? attribute.value
            : attribute.comparator == ComparatorEnum.LIKE
            ? RegExp(`^${attribute.value}`, 'i')
            : attribute.value
      };
    });
    query.where =
      data.type.toUpperCase() === ComparaisonTypeEnum.AND ? { $and: filterCriteria } : { $or: filterCriteria };
    const [result, total] = await this.repository.findAndCount({
      where: query.where,
      order: data.orders,
      ...(data.isPaginable == true || data.isPaginable == undefined
        ? {
            take: queryTake,
            skip: querySkip
          }
        : {})
    });
    return {
      data: result,
      count: total,
      ...(data.isPaginable == true || data.isPaginable == undefined
        ? {
            page: querySkip,
            totalPages: total == queryTake ? Math.trunc(total / queryTake) : Math.trunc(total / queryTake + 1)
          }
        : {})
    };
  }async search(data: QueryDto<T>): Promise<SearchResponse<T>> {
    const query: FindManyOptions<T> = { where: {} }; // initialize query to an empty object

    const queryTake = +data.take || PaginationConstants.DEFAULT_TAKE;
    const querySkip = +data.skip || PaginationConstants.DEFAULT_SKIP;
    const filterCriteria = data.attributes.map(attribute => {
      return {
        [attribute.key]:
          attribute.comparator == ComparatorEnum.EQUALS
            ? attribute.value
            : attribute.comparator == ComparatorEnum.LIKE
            ? RegExp(`^${attribute.value}`, 'i')
            : attribute.value
      };
    });
    query.where =
      data.type.toUpperCase() === ComparaisonTypeEnum.AND ? { $and: filterCriteria } : { $or: filterCriteria };
    const [result, total] = await this.repository.findAndCount({
      where: query.where,
      order: data.orders,
      ...(data.isPaginable == true || data.isPaginable == undefined
        ? {
            take: queryTake,
            skip: querySkip
          }
        : {})
    });
    return {
      data: result,
      count: total,
      ...(data.isPaginable == true || data.isPaginable == undefined
        ? {
            page: querySkip,
            totalPages: total == queryTake ? Math.trunc(total / queryTake) : Math.trunc(total / queryTake + 1)
          }
        : {})
    };
  }
Let’s start by decoupling the elements :
  • QueryDto:
    • The QueryDto class is a data transfer object (DTO) that represents the search query parameters.
    • It contains properties such as attributes, take, skip, type, orders, and isPaginable, which are used to define the search criteria.
    • Each property is decorated with validation decorators from the class-validator library to enforce data validation rules.
  • AttributeDto:
    • The AttributeDto class represents an attribute used for filtering in the search query.
    • It has properties such as key, comparator, and value.
    • The properties are decorated with validation decorators to enforce data validation rules.
  • Method:
    • The search method in the ProductService performs the actual search operation based on the provided data parameter.
    • It starts by initializing an empty query object of type FindManyOptions<T>.
    • It extracts the take and skip properties from the data parameter, which defines the pagination parameters for the search.
    • It iterates over the attributes array in data and constructs the filter criteria for the search query.
    • Based on the type property in data, it constructs the appropriate query structure using $and or $or operators to combine the filter criteria.
    • It calls the findAndCount method of the repository to execute the search query, passing the constructed where, order, take, and skip parameters.
    • It returns an object containing the search results (data) and the total count (count).
    • If isPaginable is true or undefined, it also includes pagination information such as page and totalPages in the result object.
The search method in the controller receives a QueryDto object representing search parameters. It then invokes the search method in the service, passing the QueryDto object. The service method constructs a search query based on the provided parameters and executes it using the repository's findAndCount method. The result, along with optional pagination information, is returned from the service method and propagated back to the controller, which ultimately returns the result to the client.

Section 2: Implementation 🔗

So before going deeper into the implementation part, let me clarify one thing, for the sake of maximizing the use of various scenarios, the modules are developed this way :
  • The Category Module fully implements the base module.
  • The Product Module fully implements the base module and overrides the create method.
  • The Users Module does not extend the base module at all.
A simple way to capture the implementation is this :
Entity and DTOs
We will be extending the entity and dtos from the base classes that we previously created,
our entity will look something like this :
typescript
import { Exclude, Expose } from 'class-transformer';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from '../../../shared/base/base.entity';

@Entity('category')
@Exclude()
export class CategoryEntity extends BaseEntity {
  @Column()
  @Expose()
  name: string;

  @Column()
  @Expose()
  quantity: number;
}
Now let’s implement it in our category entity.
By extending the BaseEntity, the CategoryEntity inherits properties and behaviors defined in the BaseEntity, such as _id, isDeleted, createdAt, userCreated, userUpdated, and lastUpdateAt. It also adds two additional properties, name and quantity, specific to the category entity.
This approach allows the CategoryEntity to reuse common functionality provided by the BaseEntity, such as serialization/deserialization transformations, lifecycle hooks, and more. It promotes code reusability and maintainability within the NestJS project.
Same case for dtos, you can manipulate them to your liking and extend from base-dtos if you want, even though it’s not mandatory, and add class validators.
We apply the same logic for the create and update DTOs.
The same goes for the create DTO :
typescript
export class CategoryCreateDto extends BaseCreateDto {
  @ApiProperty()
  @IsNotEmpty()
  @IsString()
  @Length(4, 30)
  name: string;

  @ApiProperty()
  @IsOptional()
  @IsNumber()
  quantity: number;
}
And the update DTO
typescript
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional, IsString, Length } from 'class-validator';
import { BaseUpdateDto } from '../../../shared/base/dtos/update-base.dto';

export class CategoryUpdateDto extends BaseUpdateDto {
  @ApiProperty()
  @IsOptional()
  @IsString()
  @Length(4, 30)
  name: string;

  @ApiProperty()
  @IsOptional()
  @IsNumber()
  quantity: number;
}
Controller
After creating the category module, we go to the controller and we start extending from our base controller.
typescript
@Controller('categories')
@ApiTags('categories')
export class CategoryController extends BaseController<CategoryEntity, CategoryCreateDto, CategoryUpdateDto>(
  CategoryCreateDto,
  CategoryUpdateDto
) {
  constructor(private readonly categoryService: CategoryService) {
    super(categoryService);
  }
}
  • The CategoryController class extends the BaseController class, which provides common CRUD operations for the CategoryEntity.
  • The generic type parameters of the BaseController class are specified as follows:
    • CategoryEntity: Represents the entity type for the controller.
    • CategoryCreateDto: Represents the DTO for creating a new category, specific to the CategoryEntity entity.
    • CategoryUpdateDto: Represents the DTO for updating an existing category, specific to the CategoryEntity entity.
    • The super(categoryService) statement is called within the constructor, invoking the constructor of the parent BaseController class. This ensures that the CategoryController has access to the common CRUD operations defined in the BaseController.
Parameters:
  • The parameters CategoryCreateDto and CategoryUpdateDto provided in the extends statement represent the DTO classes used for creating and updating category entities, respectively.
  • These values are passed to the BaseController as type arguments to specialize it for the CategoryEntity entity.
So to sum up, the CategoryController class extends the BaseController class, specializing it for the CategoryEntity entity and providing the specific DTOs for creating and updating category entities. This allows the CategoryController to inherit common CRUD operations from the BaseController and handle category-specific logic through the CategoryService class.
Service
Last but not least, we finish off the extending the baseService inside our service :
typescript
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService } from '../../shared/base/base.service';
import { IGetUserAuthInfoRequest } from '../../shared/user-request.interface';
import { CategoryCreateDto, CategoryUpdateDto } from './dtos';
import { CategoryEntity } from './entities/category.entity';

@Injectable()
export class CategoryService extends BaseService<CategoryEntity, CategoryCreateDto, CategoryUpdateDto> {
  constructor(
    @InjectRepository(CategoryEntity)
    private readonly categoryRepository: Repository<CategoryEntity>,
    @Inject(REQUEST) public readonly request: IGetUserAuthInfoRequest
  ) {
    super(categoryRepository, request);
  }
}
Certainly! Let's break down the code and explain each part:
  • The CategoryService extends the BaseService.
  • The CategoryService class has a constructor that takes two parameters,
    • The first parameter is categoryRepository, of type Repository<CategoryEntity>, which represents the repository specific to the CategoryEntity entity. It is injected using the @InjectRepository(CategoryEntity) decorator.
    • The second parameter is request, of type IGetUserAuthInfoRequest, which represents the user authentication information retrieved from the request. It is injected using the @Inject(REQUEST) decorator.
  • The constructor passes the categoryRepository and request parameters to the super keyword, which calls the constructor of the BaseService class. This ensures that the base service functionality is properly initialized with the specific repository and request information.
This module implements the abstraction layer the same way, the only difference that, the only thing is that inside the baseController and the baseService, I overrode the post method by a custom method I coded inside the classes, so for the productController, I wrote this instead :
typescript
@Post()
async create(@Body() dto: ProductCreateDto): Promise<ProductEntity> {
  return this.productService.create(dto);
}
And for the productService, I wrote this instead :
typescript
async create(dto: ProductCreateDto): Promise<ProductEntity> {
  const newProduct = Object.assign(new ProductEntity(), dto);
  newProduct.isDeleted = false;
  return await this.productRepository.save(newProduct);
}
The userEntity, which does not extend from the baseEntity, alongside the attributes that it has
typescript
import { Entity, Column, BeforeInsert, ObjectIdColumn, Index, AfterLoad, BeforeUpdate } from 'typeorm';
import { Exclude, Expose, Transform } from 'class-transformer';
import { ObjectID } from 'mongodb';
import * as crypto from 'crypto';

import { transformEntity } from '../../../shared/transformEntity.utlis';

@Entity('user')
@Exclude()
export class UserEntity {
  @Expose()
  @Column()
  @Index({ unique: true })
  email: string;

  @Column()
  password: string;

  @Expose({ groups: ['user'] })
  @Column()
  resetPasswordToken?: string;

  tempPassword?: string;
  /**************** ACTIONS ****************/
  @AfterLoad()
  private loadTempPassword(): void {
    this.tempPassword = this.password;
  }

  @BeforeInsert()
  @BeforeUpdate()
  private beforeActions() {
    if (this.tempPassword !== this.password) {
      this.password = crypto.createHmac('sha256', this.password).digest('hex');
    }
    delete this.tempPassword;

    this.lastUpdateAt = new Date();
  }

  @BeforeInsert()
  private beforeInsertActions() {
    this.status = true;
    this.createdAt = new Date();
  }
}
We’ve seen probably the logic behind the properties and decoratos in previous baseEntity.
There are two parts of methods written inside the base service, ones are to be distributed to our APIs in the controller, and local ones that we will be using for our service methods.
generateResetPasswordJWT
typescript
generateResetPasswordJWT(user) {
  return jwt.sign(
    {
      email: user.email,
      username: user.username
    },
    process.env.SECRET,
    { expiresIn: process.env.RESET_PASSWORD_EXPIRATION || '24h' }
  );
}
Generates a reset passwort token that lasts for 24 hours, you can set the duration to your liking.
findByEmail
typescript
/**
 * Find user by email
 * @param email to search (unique index)
 * @throws Error if user not found
 * @returns {IUser} populated user
 */
async findByEmail(email: string): Promise<IUser> {
  // throws error 404 if not found
  const user = await findByField(this.userRepository, { email }, true);
  return this.populateUsers(user);
}
This method retrieves a single entity from the database based on the provided email parameter. It uses the findByField function to perform the search and throws an error if the entity is not found(Optional). The retrieved entity is then passed to the populate method, and the result is returned as a Promise that resolves an interface of type IUser.
populateUsers
typescript
/**
 * Populate entit(y|ies) passed in paramter with "userCreated" and "userUpdated" properties
 * @param entities Array of entities OR an entity to populate
 * @returns Entit(y|ies) populated
 */
public async populateUsers(entities: Array<any> | any): Promise<Array<any> | any> {
  if (!entities) return;

  let tmp = entities;
  if (!Array.isArray(tmp)) tmp = [tmp];

  for (const idx in tmp) {
    const { userCreated, userUpdated } = tmp[idx];

    if (userCreated)
      tmp[idx].userCreated = await findByField(this.userRepository, { _id: userCreated?._id ?? userCreated });

    if (userUpdated)
      tmp[idx].userUpdated = await findByField(this.userRepository, { _id: userUpdated?._id ?? userUpdated });
  }

  return Array.isArray(entities) ? tmp : tmp[0];
}
Does the same thing as the populate method written in the baseService, you can check that out for further information.
findAll
typescript
async findAll(): Promise<UserEntity[]> {
  const users = await this.userRepository.find({ status: true, isDeleted: false });
  return await this.populateUsers(users);
}
This method returns the list of not deleted users and the ones with the status: true (which means that this user is allowed to login and not banned or disabled).
create
typescript
async createUser(dto: UserCreateDto): Promise<IUser> {
  const newUser = Object.assign(new UserEntity({}), dto);
  newUser.isDeleted = false;
  return await this.userRepository.save(newUser);
}
Creates a user, nothing fancy.
update
typescript
async update(toUpdate: UserEntity, dto: UserUpdateDto): Promise<IUser> {
    toUpdate.userCreated = this.request.user._id;
    toUpdate.userUpdated = this.request.user._id;
    Object.assign(toUpdate, dto);
    return this.populateUsers(await this.userRepository.save(toUpdate));
  }
Updates a user after patching the new values and setting the new current user _id values.
deleteUser
typescript
async delete(id: ObjectID): Promise<IUser> {
  // throws error 404 if not found
  const user = await findByField(this.userRepository, { id }, true);
    user.status = false;
    user.isDeleted = false;
  return await this.userRepository.save(user);
}
This method applies logical deletion to a user by setting the status and isDeleted boolean values to false.
login
typescript
async login(loginUserDto: LoginUserDto): Promise<UserEntity> {
  const findOneOptions = {
    email: loginUserDto.email,
    password: crypto.createHmac('sha256', loginUserDto.password).digest('hex'),
    status: true,
    isDeleted: false
  };

  let authUser = await this.userRepository.findOne(findOneOptions);
  if (!authUser) throwError({ User: 'Not found' }, 'Authentication failed', 401);

  // if logged delete reset password token
  if (authUser.resetPasswordToken) {
    authUser.resetPasswordToken = undefined;
    authUser = await this.userRepository.save(authUser);
  }

  delete authUser.password;
  delete authUser.tempPassword;

  return authUser;
}
this method handles the login, we will destruct the incoming DTO and apply the createHmac method on the password that creates and returns an Hmac object that uses the given algorithm and key. If the user is found, it removes the reset password token if present, deletes sensitive password-related properties, and returns the authenticated user. If no user is found, it throws an error indicating authentication failure.
generateJWT
typescript
generateJWT(user) {
    return jwt.sign(
      {
        id: user.id,
        username: user.username,
        email: user.email,
        roles: user.roles
      },
      process.env.SECRET,
      { expiresIn: process.env.TOKEN_EXPIRATION || '15d' }
    );
  }
This method generates a token that expires in 15 days, you can set the duration to your liking.
typescript
import { NestMiddleware, Injectable } from '@nestjs/common';
import { Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';
import { IGetUserAuthInfoRequest } from '../../../shared/user-request.interface';
import { throwError } from '../../../shared/throw-error.utils';
import { UsersService } from '../users.service';
import { SECRET } from '../../../config';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private readonly userService: UsersService) {}

  async use(req: IGetUserAuthInfoRequest, res: Response, next: NextFunction) {
    // get token from bearer token
    const token = req.headers?.authorization?.split(' ')[1];
    if (token) {
      try {
        const { email } = jwt.verify(token, SECRET);
        //throws Error if user not found
        const user = await this.userService.findByEmail(email);
        // set populated user in request
        req.user = user;
        next();
      } catch (error) {
        console.log(error)
        if (error.name === 'TokenExpiredError') throwError({ user: 'Invalid Token' }, 'Token Expired', 401);
        throwError({ user: 'Invalid Token' }, 'Invalid Token', 401);
      }
    } else throwError({ user: 'Not Authorized' }, 'Unauthorized', 401);
  }
}
The AuthMiddleware class is an injectable middleware class that handles the authentication logic. It extracts the token from the request header, verifies and decodes the token, retrieves the user based on the token information, and sets the user object in the request for further processing. If any errors occur, appropriate error messages are thrown.
The previously coded middleware we’ll serve greatly our application.
The application contains methods that shouldn’t be accessible to unauthorized users, that’s where the user’s module interferes, every request that we’ll be sent to our backend will be intercepted by our module, and if the API URL fits one of the paths that we have specified into our module, then our middleware will be applied, if not, the method is ready for use without user authentification.
This is our UsersModule :
typescript
import { UserEntity } from './entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { HttpModule, MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { AuthMiddleware } from './middleware/authentification.middelware';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity]), HttpModule],
  providers: [UsersService],
  controllers: [UsersController],
  exports: [UsersService]
})
export class UsersModule implements NestModule {
  public configure(consumer: MiddlewareConsumer) {
    consumer.apply(AuthMiddleware).forRoutes(
      { path: 'users', method: RequestMethod.GET },
      { path: 'users/*', method: RequestMethod.GET },
    );
  }
}

Shipping for deployment 🔗

Now that we are ready to deploy our app, we should simply just add one thing.
By checking this article provided by Heroku :
There is one line that concerns the app that we’re building,
“Heroku apps include a Procfile that specifies the commands that are executed by the app on startup.”
That’s why we should create a file called Procfile, place it underneath the root directory and add to it the simple line :
powershell
web: npm run start:prod

Done! 🔗

There you have it, this is how this boilerplate was created, you can clone this repository and start working on it. I will be checking the comments below to help the ones who encountered some problems or to answer your questions.
The project has been pushed to git through atomic commits, you can follow the steps to understand furthermore how it was built.
The GitHub project contains documented APIs thanks to Swagger.
You now have a production and a development ready environment that you can use to develop your NestJS applications.
To facilitate for you the access and comprehension of the APIs, you can access this URL to see the list of the APIs and therefore choose to perform your actions there or just use it to document your APIs.
I also added a Postman collection file that you can use.
I will be covering more projects in the future so stay tuned!
Below is a list of features I’d like to implement in the future.

To do 🔗

Software is never done, the list below contains features that would be cool implemented :

Bits of Advice 🔗

  • Don’t focus on writing the cleanest of codes when you’re in the early stages of development.
  • Always try the specify the version of the package that you want to install, and look for stable releases.
  • Testing the app is performed by the developers, it’s your job to do them, doing it in this project identified some bugs and some 500 errors 🥶.
  • Tap into other aspects of development and try reading about design patterns.
  • Write good comments.
  • Reading the documentation most of the time is better than reading Stackoverflow.
Brahim Abdelli