nestjs-rest-querynestjs-rest-query

Integrando com o Swagger UI

Como o @ApiDynamicQuery gera parâmetros OpenAPI a partir das regras compiladas, e por que o interceptor de requisição é necessário.

O @ApiDynamicQuery(rules) recebe o objeto que o defineQueryRules(...) devolveu e faz duas coisas com ele: registra-o como as regras de autorização do endpoint e gera os parâmetros OpenAPI. Um objeto, dois usos — e é esse o ponto: a documentação não pode descrever um endpoint que a autorização não cobra.

Não é genérico, e é compilado fora da classe

O @ApiDynamicQuery perdeu o parâmetro de tipo na v3. Um @ApiDynamicQuery<User>({ filters: [...] }) que sobrou dá TS2558: Expected 0 type arguments, but got 1.

Ele também é um decorator de método, avaliado quando a classe do controller carrega — antes de o Nest ter container ou repositório. Então as regras têm de ser compiladas no escopo do módulo, em arquivo próprio. Em particular, a receita buildSchemaRegistry(repository) do TypeORM não compõe com ele, porque ainda não existe repositório.

Pré-requisitos

  • @nestjs/swagger ^11 instalado (é peer dependency opcional)
  • SwaggerModule inicializado no bootstrap com DocumentBuilder
  • query parser configurado como extended no Express (necessário para os filtros aninhados)
  • dqbSwaggerRequestInterceptor registrado nas opções do SwaggerModule.setup, para testar filtros pela UI
pnpm add @nestjs/swagger
main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { dqbSwaggerRequestInterceptor } from 'nestjs-rest-query';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // Necessário para que filter[campo][op]=valor seja lido como objeto aninhado
  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: {
      // Necessário para testar filtros aninhados direto na Swagger UI
      requestInterceptor: dqbSwaggerRequestInterceptor(document),
    },
  });

  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();

Se o @nestjs/swagger não estiver instalado, o @ApiDynamicQuery continua registrando as regras e simplesmente não emite decorator OpenAPI nenhum — o import é protegido.

No controller

users.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import {
  ApiDynamicQuery,
  ApiPaginatedResponse,
  DynamicQueryDto,
  QueryRules,
  type CompiledQueryRules,
} from 'nestjs-rest-query';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
// Compilado no carregamento do módulo, em arquivo próprio.
import { userRules } from './users.query';

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

  @Get()
  @ApiDynamicQuery(userRules)
  @ApiPaginatedResponse(User, { description: 'Lista de usuários' })
  findAll(
    @Query() query: DynamicQueryDto,
    @QueryRules() rules: CompiledQueryRules
  ) {
    return this.usersService.findAll(query, rules);
  }
}

O @ApiPaginatedResponse(model, options?) documenta o envelope de resposta — data mais page, perPage, total, lastPage. A assinatura dele não mudou na v3.

O que é gerado

ParâmetroEmitidoA descrição carrega
pagesempre—
perPagesempre—
paginatesalvo quando as regras o proíbemo teto de linhas sem paginação
sortquando sorts não está vaziotodo path ordenável, mais um exemplo ascendente e um descendente
fieldssempre (fields.root é obrigatório)o allowed do root mais o allowed de cada relação, como paths pontuados
includesquando includes não está vaziotodo path de relação incluível
searchquando search não está vazioos paths pesquisados
filterquando filters não está vaziotodo path filtrável e uma tabela de operadores

A tabela de operadores é a união do que os campos do endpoint autorizam — não existe lista global de operadores na v3 para servir de fallback. Quando um endpoint não declara filtro nenhum, a lista completa de 14 é mostrada em vez de uma tabela vazia.

Só as formas externas são descritas. O OpenAPI não tem como expressar "filter[price] aceita gt mas filter[name] não", então o filter é documentado como um parâmetro string cuja descrição lista os paths e os operadores. A regra autoritativa continua sendo o objeto de regras compiladas, e uma combinação não autorizada é 400 com um code legível por máquina.

As descrições dos parâmetros gerados estão hoje escritas em português (Numero da pagina., Ordenacao dos resultados., ...). Elas ainda não são localizáveis.

Resultado esperado

Parâmetros gerados

Swagger UI com parâmetros dinâmicos

Resposta da consulta

Swagger UI com a resposta da consulta

Por que o requestInterceptor é necessário

A Swagger UI monta a URL dos parâmetros de forma diferente do formato que a biblioteca espera. O dqbSwaggerRequestInterceptor intercepta a requisição antes de ela sair e remonta os filtros na forma filter[campo][op]=valor, que o qs — o parser de query estendido do Express — consegue expandir num objeto aninhado.

Sem ele, os filtros chegam malformados ao controller e são ignorados. Para Postman, um frontend, ou qualquer cliente que monte a URL sozinho, o interceptor não é necessário.

O campo do formulário aceita uma expressão ([name][eq]=Ada ou filter[name][eq]=Ada) ou várias unidas por & ([name][eq]=Ada&[score][gt]=10).

O @nestjs/swagger não chama o interceptor no servidor: ele o grava no swagger-ui-init.js com fn.toString(), e o browser executa esse texto sem nada deste pacote no escopo. Por isso a função que dqbSwaggerRequestInterceptor(document) devolve é totalmente autocontida — as rotas marcadas viajam dentro do próprio código, como literal. No 3.0.0-alpha.0 ela era uma closure sobre helpers do módulo, e todo "Try it out" falhava com ReferenceError: interceptSwaggerRequest is not defined; atualize em vez de copiar um contorno para a sua aplicação.


Se você não usa Swagger, troque @ApiDynamicQuery por @DynamicQuery — o registro das regras e o comportamento do @QueryRules() são idênticos, sem gerar decorator OpenAPI nenhum.

Editar esta página no GitHub

On this page