Installation
How to install and configure nestjs-rest-query in your project.
Install the package
pnpm add nestjs-rest-query@alphaThe @alpha tag is not optional while 3.x is a prerelease. It currently
resolves to 3.0.0-alpha.0, which is the API these pages describe; latest
still points at 2.1.0, so dropping the tag installs the 2.x API instead
and none of the code here will compile against it.
Then install the ORM you use. All ORM peers are optional and the root entrypoint loads none of them, so you install exactly one:
pnpm add typeorm @nestjs/typeormConfigure 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. Without it a filter is silently accepted and
never applied.
2. ValidationPipe with implicit conversion
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
})
);DynamicQueryDto types page, perPage and paginate as string, because
that is what arrives on the wire. The library parses and validates them itself —
?page=abc is a 400 PAGINATION_INVALID, never a silent fallback to page 1.
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('My 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. Every
option is optional — forRoot({}) gives you the defaults:
import { Module } from '@nestjs/common';
import { DynamicQueryBuilderModule } from 'nestjs-rest-query';
@Module({
imports: [
DynamicQueryBuilderModule.forRoot({
pagination: { defaultPerPage: 10, maxPerPage: 500 },
}),
],
})
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.
No adapter here
forRoot configures common policy only. There is no adapter option and no
implicit default adapter: the adapter is decided by the source you pass to
execute(). Passing adapter or operators is refused at startup with
SOURCE_CONFIGURATION_INVALID and the message
forRoot no longer accepts "adapter"; see the "2.x → 3.x" section of MIGRATION.md.
Configuration options
forRoot accepts a QueryBuilderConfigV3 object.
pagination
| Option | Type | Default | Description |
|---|---|---|---|
defaultPerPage | number | 10 | Items per page when the request carries no perPage. |
maxPerPage | number | 500 | Upper bound. A larger perPage is a 400, not a silent clamp. |
allowUnpaginated | boolean | true | false refuses ?paginate=false with a 400 on every endpoint. |
maxUnpaginatedRows | number | maxPerPage | Row cap of a paginate=false response; above it the request is a 400, not cut. |
All are validated at startup: defaultPerPage below 1, a maxPerPage
smaller than defaultPerPage, a non-boolean allowUnpaginated or a
maxUnpaginatedRows that is not a positive integer throws
SOURCE_CONFIGURATION_INVALID while the module loads. An endpoint can replace
the two unpaginated keys in its rules — see
Pagination.
logging
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enables internal logs. |
level | 'error' | 'warn' | 'info' | 'debug' | 'info' | Minimum emitted level. |
redactValues | boolean | true | Redacts filter and search values before logging. |
logger | LoggerLike | NestJS Logger | Any object with error/warn/log/debug works (winston, pino, ...). |
DynamicQueryBuilderModule.forRoot({
logging: { enabled: true, level: 'debug' },
});The plan log at debug carries metadata only — paths, operators, counts,
pagination — never the values the client sent.
portability
| Option | Type | Default | Description |
|---|---|---|---|
enforce | boolean | false | Requires the source to carry certified profile facts, and checks them. |
With enforce: true, a source built without portabilityProfile is refused
with PORTABILITY_PROFILE_MISMATCH, and so is a profile whose dialect does not
match the adapter's or that fails checkPortabilityProfile(). Collect the facts
once at startup with collectProfileFacts() and hand them to the source
factory — the request path never queries catalogs.
consistency
| Option | Type | Default |
|---|---|---|
consistency | 'eventual' | 'transactional' | 'eventual' |
No shipped adapter supports 'transactional' today, so forRoot refuses
the value: TypeORM, Prisma and Drizzle all report transactionalConsistency: false, and a setting that would fail every request dies at startup with
SOURCE_CONFIGURATION_INVALID instead. 'eventual' is the only value that
boots.
textProfile
| Option | Type | Default |
|---|---|---|
textProfile | 'portable-strict' | 'database-native' | 'portable-strict' |
Under portable-strict, ilike, notIlike and search compare a declared
folded column against a folded term, so no ILIKE and no
mode: 'insensitive' is ever emitted.
'database-native' is reserved, and forRoot refuses it with
SOURCE_CONFIGURATION_INVALID. No adapter reads the value — folding is
applied by the core either way — so accepting it would have meant silently
losing the portability.enforce check while still compiling
portable-strict. 'portable-strict' is the only value that boots.
Next steps
With the module registered, inject QueryBuilderService into any provider,
declare a schema and endpoint rules, and pass a source to execute().
Continue to First endpoint for a working route end to end, or Adapters to see what each ORM needs.