nestjs-rest-querynestjs-rest-query
Usage GuideUsage Guide

Usage Guide

How to use nestjs-rest-query in NestJS controllers — decorators, DTOs, service, and response structure.

If you do not have a working endpoint yet, start with First endpoint. This page is the reference for decorators, parameters, operators, and the security whitelist.

Decorators

@ApiDynamicQuery(rules)

Method decorator for endpoints that need Swagger documentation. It does two things simultaneously:

  • Stores RulesConfig as metadata on the method (read by @QueryRules() at runtime)
  • Automatically generates @ApiQuery for all supported parameters (filters, sort, fields, includes, page, perPage)
@Get()
@ApiDynamicQuery({
  filters: ['name', 'email', 'createdAt'],
  sorts: ['name', 'createdAt'],
  fields: ['id', 'name', 'email', 'createdAt'],
  includes: ['company'],
})
async findAll(/* ... */) {}

@DynamicQuery(rules)

Identical to @ApiDynamicQuery, but without Swagger generation. Use it for internal endpoints or when @nestjs/swagger is not installed.

@Get()
@DynamicQuery({
  filters: ['name', 'status'],
  sorts: ['name'],
})
async findAll(/* ... */) {}

@QueryRules()

Parameter decorator that reads the RulesConfig stored by @ApiDynamicQuery or @DynamicQuery and injects it as a method argument at runtime.

async findAll(
  @Query() query: DynamicQueryDto,
  @QueryRules() rules: RulesConfig,  // reads the whitelist from the method decorator
) {}

@QueryRules() depends on the metadata generated by @ApiDynamicQuery or @DynamicQuery on the same method. Without one of those decorators, rules will be {}.

Complete controller

users.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import {
  ApiDynamicQuery,
  ApiPaginatedResponse,
  DynamicQueryDto,
  QueryResult,
  QueryRules,
  RulesConfig,
} from 'nestjs-rest-query';
import { User } from './entities/user.entity';
import { UsersBusiness } from './users.business';

@ApiTags('users')
@Controller('users')
export class UsersController {
  constructor(private readonly usersBusiness: UsersBusiness) {}

  @Get()
  @ApiOperation({ summary: 'Lista usuários com filtros dinâmicos' })
  @ApiDynamicQuery({
    filters: ['username', 'email', 'firstName', 'lastName', 'createdAt'],
    sorts: ['username', 'email', 'createdAt'],
    fields: ['id', 'username', 'email', 'firstName', 'lastName', 'createdAt'],
  })
  @ApiPaginatedResponse(User)
  async findAll(
    @Query() query: DynamicQueryDto,
    @QueryRules() rules: RulesConfig
  ): Promise<QueryResult<User>> {
    return this.usersBusiness.findAll(query, rules);
  }
}

Service

Inject QueryBuilderService and the Repository normally through NestJS dependency injection. Since the module is @Global, the service is available in any provider without importing DynamicQueryBuilderModule again.

users.business.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
  DynamicQueryDto,
  QueryBuilderService,
  QueryResult,
  RulesConfig,
} from 'nestjs-rest-query';
import { User } from './entities/user.entity';

@Injectable()
export class UsersBusiness {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly queryBuilderService: QueryBuilderService
  ) {}

  async findAll(
    query: DynamicQueryDto,
    rules: RulesConfig
  ): Promise<QueryResult<User>> {
    return this.queryBuilderService.execute(this.userRepository, query, rules);
  }
}

Supported query parameters

ParameterFormatExample
filterfilter[field][operator]=valuefilter[email][eq]=joao@email.com
sortCSV of fields (- for desc)sort=-createdAt,name
fieldsCSV listfields=id,name,email
includesCSV listincludes=company,roles
pagenumberpage=2
perPagenumberperPage=25
paginatetrue / falsepaginate=false (returns all records)

Available filter operators

OperatorDescriptionExample
eqEqualfilter[status][eq]=active
neNot equalfilter[status][ne]=inactive
gtGreater thanfilter[age][gt]=18
gteGreater than or equalfilter[age][gte]=18
ltLess thanfilter[price][lt]=100
lteLess than or equalfilter[price][lte]=100
likeContains (case-sensitive)filter[name][like]=John
ilikeContains (case-insensitive, portable)filter[name][ilike]=joao
inIn a list (CSV)filter[status][in]=active,pending
notInNot in a list (CSV)filter[status][notIn]=deleted
betweenBetween two values (val1,val2)filter[createdAt][between]=2024-01-01,2024-12-31
isNullNull (true) or not null (false)filter[deletedAt][isNull]=true

Paginated response

execute() returns Promise<QueryResult<T>>. When pagination is active (default), the response includes pagination data at the top:

{
  "data": [
    { "id": 1, "name": "Ana Lima", "email": "ana@email.com" },
    { "id": 2, "name": "Bruno Costa", "email": "bruno@email.com" }
  ],
  "page": 1,
  "perPage": 10,
  "total": 42,
  "lastPage": 5
}

When paginate=false, the response contains only { "data": [...] }.

RulesConfig — security whitelist

Each endpoint defines its own whitelist through RulesConfig. Fields not listed in the whitelist result in 400 Bad Request — the library never executes unauthorized filters, sorts, or includes.

PropertyTypeDescription
filtersstring[]Fields that can be used in filter[field][op].
sortsstring[]Fields that can be used in sort=field or sort=-field.
fieldsstring[]Fields that can be selected via fields=. Also restricts sort fields (see below).
includesstring[]TypeORM relations that can be loaded via includes=.
aliasstringEntity alias in QueryBuilder. Default: 'root'.

Warning: when fields is defined, it also restricts the fields in sorts. A field present in sorts but missing from fields will be rejected. To avoid this, keep fields and sorts in sync — or omit fields if you do not need to restrict column selection.

RulesConfig.operators can narrow the accepted filter operators for one endpoint:

@ApiDynamicQuery({
  filters: ['name', 'status'],
  operators: { allowed: ['eq', 'ilike'] },
})

This endpoint setting takes precedence over the global DynamicQueryBuilderModule.forRoot({ operators }) config.

How the whitelist protects the endpoint

Rendering Mermaid diagram...

When an unauthorized field is used in filter, sort, or includes, the response is:

{
  "message": "Filter field(s) not allowed: password. Allowed fields: username, email, firstName, lastName, createdAt",
  "error": "Bad Request",
  "statusCode": 400
}

Next steps

Edit this page on GitHub

On this page