nestjs-rest-querynestjs-rest-query
Getting StartedInstallation

Installation

How to install and configure nestjs-rest-query in your project.

Install the package

pnpm add nestjs-rest-query@alpha

The @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/typeorm

Configure 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

main.ts
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:

app.module.ts
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

OptionTypeDefaultDescription
defaultPerPagenumber10Items per page when the request carries no perPage.
maxPerPagenumber500Upper bound. A larger perPage is a 400, not a silent clamp.
allowUnpaginatedbooleantruefalse refuses ?paginate=false with a 400 on every endpoint.
maxUnpaginatedRowsnumbermaxPerPageRow 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

OptionTypeDefaultDescription
enabledbooleanfalseEnables internal logs.
level'error' | 'warn' | 'info' | 'debug''info'Minimum emitted level.
redactValuesbooleantrueRedacts filter and search values before logging.
loggerLoggerLikeNestJS LoggerAny 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

OptionTypeDefaultDescription
enforcebooleanfalseRequires 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

OptionTypeDefault
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

OptionTypeDefault
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.

Edit this page on GitHub

On this page