Estou evoluindo a arquitetura das minhas APIs e comecei a pensar mais seriamente sobre como estruturar paginação, filtros e ordenação de forma realmente escalável, como vejo em projetos maiores. A ideia é evitar aquele CRUD simples que depois vira um problema quando a API cresce.
O cenário que estou tentando resolver é este:
Tenho cerca de 50 registros de usuários e uso paginação com page e limit (ex: 10 por página). Porém também preciso suportar:
- filtros (ex:
active=true) - ordenação (ex:
createdAt DESC) - busca por texto
- metadata de paginação
- possivelmente relations
O problema conceitual que percebi é:
Se o frontend filtrar ou ordenar depois da paginação, ele só trabalha com os 10 registros da página atual e não com os 50 registros totais. Isso gera resultados inconsistentes.
Então entendi que o fluxo correto deveria sempre ser:
Database → WHERE → ORDER BY → LIMIT → OFFSET → Response
E nunca:
Database → LIMIT → frontend filter → frontend sort
Ou seja, filtro e ordenação sempre devem acontecer antes da paginação.
Estrutura de query que estou pensando em usar
Algo nesse formato:
GET /users?page=1&limit=10&active=true&sortBy=createdAt&order=DESC&search=cris
Com um DTO base tipo:
export class QueryDto {
page:number = 1;
limit:number = 10;
sortBy?:string;
order:'ASC'|'DESC' = 'DESC';
search?:string;
}
E um DTO específico:
export class UserQueryDto extends QueryDto{
active?:boolean;
}
Implementação que estou considerando no service (QueryBuilder)
Algo nessa linha:
const qb =
this.repository.createQueryBuilder('user');
if(active){
qb.andWhere(
'user.active = :active',
{active}
);
}
if(search){
qb.andWhere(
'user.name ILIKE :search',
{search:`%${search}%`}
);
}
qb.orderBy(
`user.${sortBy}`,
order
);
qb.take(limit);
qb.skip(offset);
const [data,total] =
await qb.getManyAndCount();
Response pattern que estou pensando em padronizar
{
"data": [],
"meta": {
"total":48,
"perPage":10,
"currentPage":1,
"totalPages":5,
"currentPageCount":10,
"hasNext":true,
"hasPrevious":false
}
}
Também pensei em talvez incluir links (first, last, next, previous), mas ainda estou avaliando se vale a pena.
Ideia de arquitetura que estou considerando
Criar algo reutilizável como:
- PaginationHelper
- BaseCrudService
- BaseQueryDto
- Filter DTO por entidade
Para evitar duplicar lógica em todos os services.
Mas não sei até que ponto isso ajuda ou vira overengineering cedo demais.
Dúvidas que queria ouvir opiniões de quem já trabalhou com APIs maiores:
1) Vocês preferem usar repository.find() enquanto dá ou já partem direto para QueryBuilder quando existe filtro dinâmico?
2) Vocês deixam sortBy livre ou fazem whitelist de campos permitidos?
3) Preferem separar:
- PaginationDto
- FilterDto
- SortDto
Ou usar um único QueryDTO?
4) Vale a pena criar um BaseCrudService genérico ou isso costuma virar complexidade desnecessária?
5) Como vocês estruturam filtros mais complexos como:
- date ranges
- LIKE search
- múltiplos filtros combinados
6) Vocês deixam o frontend enviar qualquer filtro ou criam um mapper controlando o que pode ser filtrado?
7) Em projetos maiores vocês costumam usar algo como:
- Specification Pattern
- Query Objects
- Custom repositories
- CQRS
- outro padrão?
Objetivo
Estou tentando sair de CRUD básico e começar a estruturar um padrão mais próximo do que vejo em projetos mais maduros, principalmente para evitar retrabalho quando a API começar a crescer.
Queria muito entender:
- como vocês fazem em produção
- o que vale a pena já fazer cedo
- o que vocês evitariam se começassem de novo
- erros comuns nesse tipo de arquitetura
Se alguém puder compartilhar experiências reais ou padrões que funcionaram bem seria muito útil.
Stack atual:
- NestJS
- TypeORM
- PostgreSQL
- Axios no frontend
Qualquer insight ou experiência prática já ajuda bastante.
Top comments (0)