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
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
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);
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');
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;
}
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;
}
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';
}
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;
}
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();
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 },
}),
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"}}
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)