DEV Community

Cover image for Advanced Filtering with Nestjs: The Easy Way
Ruben Alvarado
Ruben Alvarado

Posted on

Advanced Filtering with Nestjs: The Easy Way

When building an API, you often need to handle complex query parameters. If you're just coding casually, this might not be a concern, but when building a real-world solution, pagination, filtering, and sorting are essential for creating a robust and scalable API.

In my current project with NestJs, I faced this challenge. Initially, I thought, "Easy, I just need to write the DTOs"—but it wasn't that simple. Today, I'm going to show you how to implement complex filtering in a NestJs endpoint.

NestExpressApplication

First, we need to create a new Nest project. If you've already installed the CLI, this is a simple step. If not, I highly recommend installing it.

nest new complex-query-filters
Enter fullscreen mode Exit fullscreen mode

Since we'll need to validate and transform our DTOs, let's install class-validator and class-transformer packages.

npm i class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode

There you have it—a minimal NestJs API. However, to handle complex queries, we need to make a small change in our project: declaring the app as a NestExpressApplication. You might ask, "Isn't it an Express app by definition?" And you're right, but explicitly declaring the app type gives us access to Express-specific utilities.

const app = await NestFactory.create<NestExpressApplication>(AppModule);
Enter fullscreen mode Exit fullscreen mode

Now, we can define the query parser of our application, letting Nest know that our application is going to handle complex query parameters.

app.set('query parser', 'extended');
Enter fullscreen mode Exit fullscreen mode

Validations, A Pragmatic Programer Always Include Validations

That's it! Now your Nest app handles complex queries like: dateRange[lte]=2023-10-26T10:00:00.000Z&search=something. But what happens if someone sends a SQL injection or something that could crash your app? As software engineers, we need to ensure we're building robust applications.

To accomplish this, we'll follow a simple structure by creating DTOs that tell our server which properties we want to receive. Let's start with the search fields. Create a new file with a class called SearchFieldsDto that will define the valid properties clients can search by. I'll keep it simple by declaring just two properties.

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

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

For a more detailed look at the validations we're using, I highly recommend checking the class-validator docs. Now, let's build our FiltersDto.

Building the FiltersDto

To keep this post concise, I'll add some simple filters to demonstrate the approach. Let's create our FiltersDto class with three properties: status, category, and dateRange. We'll also create another class for the operators that can filter by date range.

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;
}
Enter fullscreen mode Exit fullscreen mode

With this structure, we're telling our Nest application that it can handle requests with nested attributes, such as: dateRange[lte]=2023-10-26T10:00:00.000Z.

The QueryDto Class

Finally, let's combine everything into a single QueryDto class. We'll add page and limit attributes, plus a sort property that specifies which fields we can sort by and in which direction. First, let's create the SortFieldsDto.

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

Now when someone sends the sort parameter, it can only sort by category and only accept the values "asc" or "desc"—something like this: sort[category]=asc. Let's combine everything into our QueryDto class, which will extend our 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;
}
Enter fullscreen mode Exit fullscreen mode

We could separate everything into its own DTO and group them with the Intersection utility, but I think this approach better demonstrates how to structure the DTO visually. So there you have it—your API is now validated and prevents SQL or RSQL injections... or does it? Well, not yet.

ValidationPipe Comes to the Rescue

If you send a request like dateRange[lt]=today&search[magumbos]=something with the current project setup, NestJS will accept it as valid because we haven't explicitly defined our validation and transformation rules yet. To fix this, let's go to the main.ts file and add the useGlobalPipes method before the app.listen line.

app.useGlobalPipes();
Enter fullscreen mode Exit fullscreen mode

This method requires a ValidationPipe instance to function. The validation instance accepts an object containing key-value pairs that define our validation and transformation rules for the entire project. You can configure these rules at different levels (per module, controller, etc.), but that's a topic for another post.

new ValidationPipe({
  whitelist: true,
  transform: true,
  forbidNonWhitelisted: true,
  transformOptions: { enableImplicitConversion: true },
}),
Enter fullscreen mode Exit fullscreen mode

With this configuration, we're instructing our validation pipe that all properties should be whitelisted, transformed, and any non-whitelisted properties should be forbidden. We're also enabling implicit object conversion. As a result, when you send a request like dateRange[lt]=today, you'll receive a bad request response indicating that the property lt should not be present.

If you send this request: http://localhost:3000/?dateRange[lte]=2023-10-26T10:00:00.000Z&search[status]=so&page=1&limit=10&sort[category]=asc, you'll get this result:

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

Final Thoughts

With this simple example, I've demonstrated the importance of being a software developer rather than relying entirely on AI tools. I spent several minutes asking ChatGPT, Claude, and DeepSeek how to handle this type of query in NestJS.

They all suggested complex solutions and approaches, when I just needed to read the documentation to realize it was a one-line change. The lesson here is that while it's great to use new tools to help solve problems, there's always a need to develop solutions on your own.

Keep learning, and I'll see you in the next one. You can find all the code for this example in the following GitHub repository: https://github.com/RubenOAlvarado/complex-query-filters

Photo belongs to Luca Bravo in Unsplash

Top comments (0)