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^11instalado (é peer dependency opcional)SwaggerModuleinicializado nobootstrapcomDocumentBuilderquery parserconfigurado comoextendedno Express (necessário para os filtros aninhados)dqbSwaggerRequestInterceptorregistrado nas opções doSwaggerModule.setup, para testar filtros pela UI
pnpm add @nestjs/swaggerimport { 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
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âmetro | Emitido | A descrição carrega |
|---|---|---|
page | sempre | — |
perPage | sempre | — |
paginate | salvo quando as regras o proíbem | o teto de linhas sem paginação |
sort | quando sorts não está vazio | todo path ordenável, mais um exemplo ascendente e um descendente |
fields | sempre (fields.root é obrigatório) | o allowed do root mais o allowed de cada relação, como paths pontuados |
includes | quando includes não está vazio | todo path de relação incluível |
search | quando search não está vazio | os paths pesquisados |
filter | quando filters não está vazio | todo 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

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.

