Installation
How to install and configure nestjs-rest-query in your project.
Install the package
pnpm add nestjs-rest-queryConfigure bootstrap
Three adjustments are required in main.ts for the library to work correctly.
1. Extended query parser
app.set('query parser', 'extended');Express 5 changed the default query parser from qs (extended) to simple. The library expects parameters like ?filter[name][eq]=foo to be expanded into nested objects by the framework before reaching the controller — that only happens with the extended parser.
2. ValidationPipe with implicit conversion
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
})
);The DTO uses string for fields like page and perPage. enableImplicitConversion: true instructs class-transformer to convert those values to the expected types automatically, without requiring additional decorators in the DTO.
3. Swagger interceptor (optional)
import { dqbSwaggerRequestInterceptor } from 'nestjs-rest-query';
SwaggerModule.setup('/', app, document, {
swaggerOptions: {
requestInterceptor: dqbSwaggerRequestInterceptor(document),
},
});Swagger UI serializes filters as multiple query params (filter[name][eq]=foo). Without the interceptor, the browser sends those parameters in a way that Express may not preserve the bracket notation correctly. dqbSwaggerRequestInterceptor rewrites the URL before sending the request, ensuring the filters reach the controller in the expected format.
This interceptor is only necessary to test filters through the Swagger UI. External clients (Postman, frontend applications, etc.) build the URL directly and do not need it. The Swagger section explains the full scenario.
Full example
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { dqbSwaggerRequestInterceptor } from 'nestjs-rest-query';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Required to expand filter[field][op]=value into nested objects
app.set('query parser', 'extended');
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
})
);
const document = SwaggerModule.createDocument(
app,
new DocumentBuilder().setTitle('Minha API').setVersion('1.0').build()
);
SwaggerModule.setup('/', app, document, {
swaggerOptions: {
requestInterceptor: dqbSwaggerRequestInterceptor(document),
},
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();Register the module
Import DynamicQueryBuilderModule in the root AppModule with forRoot. All options are optional — without configuration, the library uses the default values:
import { Module } from '@nestjs/common';
import { DynamicQueryBuilderModule } from 'nestjs-rest-query';
@Module({
imports: [
DynamicQueryBuilderModule.forRoot({
// all options are optional
}),
],
})
export class AppModule {}The module is @Global, so you do not need to import it in feature modules — QueryBuilderService becomes available across the entire application automatically.
Configuration options
forRoot accepts a QueryBuilderConfig object. All properties are optional.
pagination
Controls the default pagination behavior.
| Option | Type | Default | Description |
|---|---|---|---|
defaultPerPage | number | 10 | Number of items per page when perPage is not provided in the request. |
maxPerPage | number | 100 | Maximum number of items per page, ignoring larger values sent by the client. |
operators
Restricts the allowed filter operators globally in the application.
| Option | Type | Default | Description |
|---|---|---|---|
allowed | QueryOperator[] | all allowed | List of accepted operators. undefined = all. [] (empty list) = no operators allowed. |
DynamicQueryBuilderModule.forRoot({
operators: {
allowed: ['eq', 'like', 'in', 'between'],
},
});You can also override this per endpoint with RulesConfig.operators; when present, the endpoint value wins over the global forRoot setting for that route.
@ApiDynamicQuery({
filters: ['name', 'status'],
operators: { allowed: ['eq', 'ilike'] },
})
list(@Query() query: DynamicQueryDto, @QueryRules() rules: RulesConfig) {
return this.service.list(query, rules);
}If the endpoint explicitly sets operators: {}, it resets that route to all operators allowed.
logging
Enables and configures internal query-building logs.
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enables internal logs. |
level | 'error' | 'warn' | 'info' | 'debug' | 'info' | Minimum emitted log level. |
logger | LoggerLike | NestJS Logger | Custom logger. Any object with error/warn/log/debug works (winston, pino, etc.). |
DynamicQueryBuilderModule.forRoot({
logging: {
enabled: true,
level: 'debug',
},
});Next steps
With the module registered, inject QueryBuilderService into any provider and start building dynamic queries from HTTP request parameters.
If you want to start from zero with a real endpoint, continue to First endpoint. To understand the OpenAPI integration, see Swagger.