typescript · rest-api · backend-development · nestjs

Filtrado avanzado con NestJS: la forma sencilla

5 min de lectura

La foto pertenece a Luca Bravo en Unsplash

Al construir una API, muchas veces hace falta manejar parámetros de query complejos. Si se está programando de forma casual, esto podría no ser una preocupación, pero al construir una solución real, la paginación, el filtrado y el ordenamiento son esenciales para crear una API robusta y escalable.

En un proyecto reciente con NestJS surgió este desafío. La primera idea fue: “fácil, solo hace falta escribir los DTOs” — pero no resultó tan simple. Hoy vamos a ver cómo implementar filtrado complejo en un endpoint de NestJS.

NestExpressApplication

Primero, hace falta crear un nuevo proyecto de Nest. Si ya se tiene instalado el CLI, este es un paso simple. Si no, se recomienda instalarlo.

nest new complex-query-filters

Ya que vamos a necesitar validar y transformar los DTOs, vamos a instalar los paquetes class-validator y class-transformer.

npm i class-validator class-transformer

Con esto ya se tiene una API mínima de NestJS. Sin embargo, para manejar queries complejas, hace falta un pequeño cambio en el proyecto: declarar la app como NestExpressApplication. Se podría preguntar: “¿no es una app de Express por definición?” Y la respuesta es sí, pero declarar explícitamente el tipo de la app da acceso a utilidades específicas de Express.

const app = await NestFactory.create<NestExpressApplication>(AppModule);

Ahora se puede definir el query parser de la aplicación, indicándole a Nest que la app va a manejar parámetros de query complejos.

app.set('query parser', 'extended');

Validaciones, un programador pragmático siempre incluye validaciones

¡Eso es todo! Ahora la app de Nest maneja queries complejas como: dateRange[lte]=2023-10-26T10:00:00.000Z&search=something. Pero, ¿qué pasa si alguien envía una inyección SQL o algo que podría hacer caer la app? Como ingenieros de software, hace falta asegurarse de estar construyendo aplicaciones robustas.

Para lograrlo, se va a seguir una estructura simple creando DTOs que le indiquen al servidor qué propiedades se quieren recibir. Empecemos con los campos de búsqueda. Se crea un nuevo archivo con una clase llamada SearchFieldsDto que va a definir las propiedades válidas por las que los clientes pueden buscar. Se mantiene simple declarando solo dos propiedades.

export class SearchFieldsDto {
  @IsOptional()
  @IsString()
  @IsAlphanumeric()
  @MinLength(2)
  @MaxLength(30)
  category?: string;

  @IsOptional()
  @IsString()
  @IsAlphanumeric()
  @MinLength(2)
  @MaxLength(20)
  status?: string;
}

Para un vistazo más detallado de las validaciones que se están usando, se recomienda revisar la documentación de class-validator. Ahora, vamos a construir el FiltersDto.

Construyendo el FiltersDto

Para mantener este artículo conciso, se van a agregar algunos filtros simples para demostrar el enfoque. Vamos a crear la clase FiltersDto con tres propiedades: status, category y dateRange. También se va a crear otra clase para los operadores que permiten filtrar por rango de fechas.

export class DateRangeDto {
  @IsOptional()
  @IsDateString()
  gte?: Date;

  @IsOptional()
  @IsDateString()
  lte?: Date;
}

export class FiltersDto {
  @IsOptional()
  @IsEnum(['active', 'inactive', 'pending'], {
    message: 'Status must be one of: active, inactive, pending',
  })
  @IsString()
  status?: string;

  @IsOptional()
  @IsString()
  @IsEnum(['electronics', 'furniture', 'clothing'], {
    message: 'Category must be one of: electronics, furniture, clothing',
  })
  category?: string;

  @IsOptional()
  @IsObject()
  dateRange?: DateRangeDto;
}

Con esta estructura, se le indica a la aplicación de Nest que puede manejar requests con atributos anidados, como: dateRange[lte]=2023-10-26T10:00:00.000Z.

La clase QueryDto

Finalmente, vamos a combinar todo en una sola QueryDto class. Se van a agregar los atributos page y limit, además de una propiedad sort que especifica por qué campos se puede ordenar y en qué dirección. Primero, vamos a crear el SortFieldsDto.

export class SortFieldsDto {
  @IsOptional()
  @IsString()
  @IsIn(['asc', 'desc'])
  category?: 'asc' | 'desc';
}

Ahora, cuando alguien envía el parámetro sort, solo puede ordenar por category y solo acepta los valores “asc” o “desc” — algo así: sort[category]=asc. Vamos a combinar todo en la clase QueryDto, que va a extender el FiltersDto.

export class QueryDto extends FiltersDto {
  @IsOptional()
  @IsObject()
  @ValidateNested()
  @Type(() => SearchFieldsDto)
  search?: SearchFieldsDto;

  @IsOptional()
  @IsInt()
  page?: number;

  @IsOptional()
  @IsInt()
  limit?: number;

  @IsOptional()
  @IsObject()
  @ValidateNested()
  @Type(() => SortFieldsDto)
  sort?: SortFieldsDto;
}

Se podría separar todo en su propio DTO y agruparlos con la utilidad Intersection, pero este enfoque demuestra mejor cómo estructurar el DTO visualmente. Así que ahí está — la API ya está validada y previene inyecciones SQL o RSQL… ¿o no? Bueno, todavía no.

ValidationPipe al rescate

Si se envía un request como dateRange[lt]=today&search[magumbos]=something con la configuración actual del proyecto, NestJS lo va a aceptar como válido porque todavía no se definieron explícitamente las reglas de validación y transformación. Para solucionarlo, vamos al archivo main.ts y agregamos el método useGlobalPipes antes de la línea app.listen.

app.useGlobalPipes();

Este método necesita una instancia de ValidationPipe para funcionar. La instancia de validación acepta un objeto con pares clave-valor que definen las reglas de validación y transformación para todo el proyecto. Estas reglas se pueden configurar en distintos niveles (por módulo, por controller, etc.), pero eso es un tema para otro artículo.

new ValidationPipe({
	whitelist: true,
  transform: true,
  forbidNonWhitelisted: true,
  transformOptions: { enableImplicitConversion: true },
}),

Con esta configuración, se le indica al validation pipe que todas las propiedades deben estar en la whitelist, transformarse, y que cualquier propiedad no incluida en la whitelist debe ser rechazada. También se habilita la conversión implícita de objetos. Como resultado, cuando se envía un request como dateRange[lt]=today, se recibe una respuesta bad request indicando que la propiedad lt no debería estar presente.

Si se envía este request: http://localhost:3000/?dateRange[lte]=2023-10-26T10:00:00.000Z&search[status]=so&page=1&limit=10&sort[category]=asc, se obtiene este resultado:

Hello World! Query: {"dateRange":{"lte":"2023-10-26T10:00:00.000Z"},"search":{"status":"so"},"page":1,"limit":10,"sort":{"category":"asc"}}

Reflexiones finales

Con este ejemplo simple, queda demostrada la importancia de ser desarrollador de software en lugar de depender por completo de herramientas de IA. Se pasaron varios minutos preguntándole a ChatGPT, Claude y DeepSeek cómo manejar este tipo de query en NestJS.

Todas sugirieron soluciones y enfoques complejos, cuando en realidad solo hacía falta leer la documentación para darse cuenta de que era un cambio de una sola línea. La lección aquí es que, si bien es genial usar herramientas nuevas para ayudar a resolver problemas, siempre hace falta desarrollar soluciones por cuenta propia.

Hay que seguir aprendiendo, nos vemos en la próxima entrega. Todo el código de este ejemplo está disponible en el siguiente repositorio de GitHub: https://github.com/RubenOAlvarado/complex-query-filters