Simplifying Pagination, Sortable, and Filterable Endpoints Using Builder Pattern in NestJs

Aditya Putra Pratama
4 min readMar 6, 2024

--

Puzzle generate by Dalle

When I create a RESTful API, I find myself repeatedly doing the same things, starting with creating entities, controllers, services. However, I face a problem when I have repetitive code, like pagination, filter, sorting that always has the same format and too long to code.

This made me wonder why not create a class where I only define what I need. After exploring various sources, I found an interesting article on implementing pagination and sorting filters using the Builder pattern. This approach is very appealing because, from what I’ve seen, it’s the most suitable method for me to use in my project.

Builder Pattern

The Builder design pattern is a way to build complex objects step by step. Imagine you’re making a burger. Instead of having all the ingredients thrown together in one go, you add the bun, then the patty, followed by lettuce, cheese, and so on, until your burger is ready to eat. Each step is done one at a time, and you can customize the burger at each step based on what you like.

In programming, some objects we create can be just as complex as a burger with many ingredients (or parts). If we tried to make these objects in one shot, the process would be messy, hard to manage, and even harder to customize. This is where the Builder pattern comes in handy.

With the Builder pattern, you have a ‘Builder’ that provides steps to configure and build parts of your complex object. You can call these steps in any order to customize the final object. Once all the parts are configured as you want them, the Builder creates the final object for you. This way, the process of creating an object is separated from the parts that make up the object, making it easier to understand, manage, and change in the future.

For example, if you’re building a software system that can generate different types of reports (PDF, HTML, etc.), using the Builder pattern allows you to construct a report step by step. You can specify the title, add paragraphs, insert charts, and choose the format (PDF, HTML) without messing with the complex logic of report generation. Each type of report would have its own builder that knows how to assemble it properly.

In summary, the Builder pattern helps in constructing complex objects step by step, providing control over the construction process, and making the code more flexible and easier to understand.

After excecuting this IDEA i just realize this is magic, First i create class call QueryHandler then i construct what ever i need from filter sort limit and pagination

import { BadRequestException, NotFoundException, Type } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { FindManyOptions, In, Repository } from 'typeorm';
import { IPagination } from '../dto/response.dto';
import { QueryBaseDto } from '../utils/query-base';
import {
IQueryOperations,
IRepositoryHandler,
} from './query.handler.interface';


export class QueryHandler<Q extends QueryBaseDto, T>
implements IQueryOperations<T>, IRepositoryHandler<T>
{
private repository: Repository<T>;
private q: Q;
private option: FindManyOptions<T> = {};

constructor(repository: Repository<T>, classDto: Type<any>) {
this.repository = repository;
}

query(query: Q): this {
this.q = query;
return this;
}


filter(where?: FindManyOptions<T>['where']): this {
let initialCondition = {};

if (this.q.statuses) {
initialCondition = { status: In(this.q.statuses) };
}

this.option.where = Array.isArray(where)
? where.map((item) => ({ ...initialCondition, ...item }))
: where
? { ...initialCondition, ...where }
: initialCondition;

return this;
}

sort(order?: FindManyOptions<T>['order']): this {
if (order) {
this.option.order = { ...this.option.order, ...order };
} else if (this.q.sort && this.q.order) {
this.option.order = {
...this.option.order,
[this.q.sort]: this.q.order.toUpperCase() as 'ASC' | 'DESC',
};
}
return this;
}


limit(): this {
if (this.q.limit && this.q.page) {
this.option.take = this.q.limit;
this.option.skip = (this.q.page - 1) * this.q.limit;
}
return this;
}


async pagination(): Promise<[T[], IPagination]> {
const [data, total] = await this.repository.findAndCount(this.option);
const pagination: IPagination = {
page: this.q.page,
size: data.length,
rows_per_page: this.q.limit,
total,
};
const dto = plainToInstance(this.classDto, data);
return [dto, pagination];
}


}

then call it on my service

  private handler = new QueryHandler(this.repository);
async list(query: QueryDto): Promise<IResponseDto<ResponseDto>> {
const [data, pagination] = await this.handler
.query(query) // init Query
.filter() // For Filtering base on search query
.limit() // For Pagination Limit
.sort() // For Sort using sort query
.pagination(); // the magic

return {data, pagination}
}

and it work, then i started to refactor my project and not only reusable but i And it worked. Then, I began to refactor my project, making it not only reusable but also reducing the time I spend developing my app.

Conclusion

There’s nothing wrong with seeking a faster way for app development. By doing this, I can save more time for the same tasks.

The article that inspired me.

https://www.freecodecamp.org/news/how-to-add-filtering-sorting-limiting-pagination-to-nestjs-app/
https://refactoring.guru/design-patterns/builder

Next action i will create my own npm package

--

--

Aditya Putra Pratama
Aditya Putra Pratama

Written by Aditya Putra Pratama

Exploring the intersections of technology and humanity. Seeking insights and sharing discoveries.