Se você já construiu uma API de busca um pouco mais séria, provavelmente chegou naquele endpoint:
POST /products/searchNão porque a operação cria alguma coisa. Não porque ela altera estado. Mas porque os filtros ficaram grandes demais, aninhados demais ou simplesmente ruins demais para continuar espremendo tudo na URL.
Durante anos, a escolha prática foi essa: ou transformar o GET em uma query string gigantesca, ou usar POST para uma operação que, semanticamente, continua sendo leitura.
Em junho de 2026, a IETF padronizou uma terceira opção na RFC 10008: o método HTTP QUERY.
O interessante não é simplesmente ganhar mais um verbo para decorar. QUERY preenche uma lacuna real do protocolo: ele permite enviar conteúdo no corpo da requisição como POST, mas declara formalmente que a operação é safe e idempotent como GET.
E isso muda mais coisa do que parece. Retry automático, cache, observabilidade, gateways e até a forma como uma API comunica sua intenção passam a ter uma semântica que antes simplesmente não existia.
O problema não era exatamente o GET
Para consultas simples, GET continua excelente.
GET /products?category=notebook&brand=lenovo&page=2A URL é legível, pode ser compartilhada, funciona muito bem com cache e representa claramente uma operação de leitura.
O problema aparece quando a busca cresce.
Imagine que o front-end de um catálogo precise enviar algo assim:
{
"filters": {
"and": [
{
"category": ["notebook", "desktop"]
},
{
"price": {
"gte": 3000,
"lte": 9000
}
},
{
"or": [
{
"gpu": ["rtx-5060", "rtx-5070"]
},
{
"ram": {
"gte": 32
}
}
]
}
]
},
"facets": ["brand", "processor", "gpu"],
"sort": [
{
"field": "rating",
"direction": "desc"
}
]
}Dá para inventar uma serialização para isso na query string? Claro.
Também dá para construir uma casa usando só chave de fenda.
A própria RFC lista problemas práticos de colocar consultas grandes na URI: limites variam entre clientes e intermediários, estruturas complexas exigem encoding pouco agradável e a URL tende a aparecer em mais lugares, como logs, históricos e bookmarks. A especificação HTTP recomenda suporte a URIs de pelo menos 8.000 octetos, mas isso não significa que toda a cadeia entre cliente e aplicação aceite qualquer URL desse tamanho sem problema.
Na vida real, sua requisição pode passar por CDN, WAF, proxy reverso, load balancer e framework antes de chegar no controller. Basta um componente ter um limite menor.
Então usamos POST. E funciona.
Esse é o ponto em que vale separar “funciona” de “representa corretamente o que está acontecendo”.
Uma busca complexa com POST é perfeitamente implementável:
POST /products/search
Content-Type: application/json
{
"category": ["notebook", "desktop"],
"minPrice": 3000,
"maxPrice": 9000
}O servidor recebe um body estruturado, valida, consulta o banco e devolve a resposta. Nenhum crime aconteceu.
O problema é que o método HTTP também comunica intenção para tudo que existe entre o cliente e sua aplicação.
POST não promete que a operação é somente leitura. Também não promete idempotência. Um componente genérico de infraestrutura não sabe se aquele POST pesquisa produtos, cobra um cartão ou cria um pedido.
Para ele, repetir automaticamente uma requisição pode ser perigoso.
Essa diferença é central no artigo HTTP Finally Got a QUERY Method, de Dave Amit: durante muito tempo, APIs de busca herdaram as características pessimistas de POST apenas para conseguir transportar um body.
É exatamente essa lacuna que QUERY tenta fechar.
Safe, idempotent e cacheable não são a mesma coisa
Os três conceitos aparecem juntos tantas vezes quando falamos de GET que é fácil misturá-los.
Safe significa que o cliente não está solicitando uma mudança de estado no recurso alvo. A intenção é consultar, não modificar.
Idempotent significa que repetir a mesma operação não deveria produzir um efeito diferente no estado do servidor. Isso é importante quando uma conexão cai depois de a requisição ter sido enviada e um cliente ou proxy precisa decidir se pode tentar novamente.
Cacheable significa que uma resposta pode, respeitando as regras de cache, ser armazenada e reutilizada para atender uma requisição posterior.
QUERY foi registrado pela IANA explicitamente como safe = yes e idempotent = yes. A RFC também define suas respostas como cacheáveis.
Característica | GET | QUERY | POST |
|---|---|---|---|
Safe | Sim | Sim | Potencialmente não |
Idempotente | Sim | Sim | Potencialmente não |
Body com semântica definida | Não | Sim | Sim |
Resposta cacheável | Sim | Sim | Com regras mais restritas |
Essa tabela é uma simplificação útil, mas a ideia principal é simples: QUERY ocupa o espaço que faltava entre “leitura bem definida” e “requisição com conteúdo estruturado”.
QUERY não é simplesmente “GET com body”
Essa é uma boa explicação de elevador e uma explicação técnica ruim.
A RFC 9110 não define uma semântica geral para conteúdo enviado em uma requisição GET. Em outras palavras, você não ganha um “GET avançado” só porque decidiu anexar JSON no body.
Além disso, clientes e intermediários podem não tratar esse corpo da maneira que sua aplicação espera.
QUERY possui outra semântica. O alvo da requisição não é apenas um recurso cuja representação será recuperada. O cliente está pedindo para aquele recurso executar uma operação de consulta dentro de seu escopo, usando o conteúdo enviado para definir essa consulta.
Isso parece uma diferença acadêmica até você olhar para o comportamento de infraestrutura.
Com QUERY, o protocolo sabe que aquele body faz parte da definição da consulta e sabe que a operação continua sendo segura e repetível.
Como uma requisição QUERY fica na prática
O formato é quase decepcionantemente simples:
QUERY /products
Content-Type: application/json
Accept: application/json
{
"filters": {
"category": ["notebook", "desktop"],
"price": {
"lte": 9000
}
},
"sort": "-rating"
}O corpo pode ser JSON, mas o método não é preso a JSON. O Content-Type faz parte da semântica da consulta.
Isso é importante a ponto de a RFC exigir que o servidor rejeite uma QUERY cujo conteúdo não tenha um Content-Type válido ou seja inconsistente com ele.
O servidor pode inclusive anunciar quais formatos aceita usando o novo header Accept-Query:
Accept-Query: "application/json", "application/jsonpath"Não significa que agora é uma boa ideia expor SQL bruto na sua API pública. Significa apenas que o protocolo não assume que toda linguagem de consulta do planeta seja JSON.
O detalhe mais interessante é o cache
Se duas requisições QUERY chegam para a mesma URL, mas com bodies diferentes, elas obviamente não representam a mesma consulta.
Veja:
QUERY /products
{"category":"notebook"}e:
QUERY /products
{"category":"smartphone"}Em um cache tradicional que olhasse apenas método e URI, teríamos um problema enorme: a chave visível seria praticamente a mesma.
A RFC 10008 resolve isso de forma explícita: a chave de cache de QUERY deve incorporar o conteúdo da requisição e os metadados relacionados.
Conceitualmente, você pode imaginar algo próximo disso:
function buildQueryCacheKey(
url: string,
contentType: string,
body: string,
): string {
return hash([
'QUERY',
url,
contentType,
body,
].join(':'));
}Não é uma implementação prescrita pela RFC, apenas uma forma simples de visualizar o conceito.
O cache também pode normalizar diferenças que não alterem a semântica do conteúdo antes de criar a chave. Isso é poderoso, mas cria uma responsabilidade nova: normalizar errado pode fazer duas consultas diferentes colidirem.
Ou seja: QUERY é cacheável, mas cachear QUERY é inerentemente mais complexo do que cachear GET.
Location e Content-Location resolvem dois problemas diferentes
Essa parte da RFC é especialmente boa porque permite voltar ao caminho tradicional do GET depois de uma consulta complexa.
Uma resposta pode trazer Content-Location apontando para um recurso que representa o resultado daquela operação:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /reports/results/8d183
{
"total": 418,
"items": []
}O servidor está dizendo que o resultado que acabou de entregar pode ser obtido posteriormente com:
GET /reports/results/8d183Já o header Location pode apontar para o chamado equivalent resource: um recurso que representa a própria consulta, incorporando alvo, conteúdo e metadados.
Nesse caso, fazer GET nessa URI equivale a repetir a consulta sem reenviar o body original.
A distinção importa:
Content-Location: URI associada ao resultado produzido.Location: URI que pode representar a consulta equivalente.
Essa possibilidade é ótima para relatórios, analytics e buscas caras: você envia uma consulta estruturada uma vez e o servidor pode transformar aquilo em um recurso normal, recuperável com GET, muito mais simples para CDNs e caches convencionais.
Retries ficam semanticamente mais corretos
Quem trabalha com sistemas distribuídos já sabe que “a conexão caiu” não significa necessariamente “a operação não aconteceu”.
É justamente por isso que idempotência importa.
Se um cliente envia uma consulta, perde a conexão antes de receber a resposta e sabe que o método é idempotente, ele tem uma base muito melhor para repetir a operação.
Esse tema conversa diretamente com o que já tratei no artigo sobre filas com BullMQ e Redis no NestJS: retries só são simples quando a operação foi desenhada para suportá-los.
QUERY não torna sua lógica interna magicamente perfeita, mas comunica ao protocolo que repetir aquela consulta não representa uma nova intenção de escrita.
Também melhora a observabilidade da API
O artigo da TechTarget sobre o novo QUERY traz um ponto que achei particularmente útil: um método dedicado também melhora classificação operacional.
Hoje, imagine olhar um dashboard e ver:
POST /orders
POST /checkout
POST /products/search
POST /reports/run
POST /customers/filterPara descobrir quais desses endpoints são mutações e quais são apenas consultas, você precisa conhecer a aplicação.
Com semântica consistente:
POST /orders
POST /checkout
QUERY /products
QUERY /reports
QUERY /customersO próprio método já carrega uma informação útil para logs, métricas, políticas de gateway e auditoria.
Isso não substitui tracing nem bons logs, claro. No meu artigo sobre observabilidade em Node e NestJS, a ideia central é justamente não depender de uma única camada para entender o sistema. Mas uma semântica HTTP mais precisa deixa essa camada um pouco menos ambígua.
E segurança? QUERY não esconde seus dados
Um argumento fácil de exagerar é que colocar os filtros no body seria “mais seguro” do que deixá-los na URL.
Não é tão simples.
A RFC observa que URIs tendem a ser registradas e processadas por mais intermediários do que o conteúdo da requisição. Isso pode ser uma boa razão para não colocar certos critérios diretamente na URL.
Mas o body continua podendo ser logado pela aplicação, por proxies configurados para isso ou por ferramentas de observabilidade.
Portanto:
não coloque segredo em uma QUERY achando que o body virou um cofre;
continue usando TLS;
faça redaction de campos sensíveis em logs;
trate autenticação e autorização exatamente com a mesma seriedade de qualquer outro endpoint.
E, caso o servidor gere uma URI para uma consulta que continha informação sensível, a própria RFC recomenda que essa URI não incorpore os dados sensíveis originais.
QUERY no Express 5 já é real
A documentação atual do Express 5 já expõe app.query() e router.query().
Um endpoint mínimo pode ficar assim:
import express from 'express';
const app = express();
app.use(express.json());
app.query('/products', async (req, res) => {
const filters = req.body;
const products = await searchProducts(filters);
return res.json({
data: products,
});
});
app.listen(3000);Como o conteúdo da consulta está no body, o parser continua necessário. Sem express.json(), req.body não será populado para JSON.
A própria documentação do Express faz um alerta importante: suporte de clientes e intermediários ainda é limitado e o ambiente precisa ser validado antes de depender de QUERY em produção.
OpenAPI 3.2 também já entende QUERY
OpenAPI 3.2 adicionou query como uma operação válida de um Path Item.
Um contrato poderia ser descrito assim:
openapi: 3.2.0
paths:
/products:
query:
summary: Pesquisa produtos com filtros avançados
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
category:
type: array
items:
type: string
maxPrice:
type: number
responses:
'200':
description: Resultado da pesquisaA especificação OpenAPI 3.2 referencia o método QUERY e seu sucessor RFC, o que é um passo importante para documentação, geração de clientes e tooling.
Mas existe uma diferença grande entre a especificação aceitar um campo e todo o ecossistema que consome OpenAPI já suportá-lo corretamente. Um gerador de SDK antigo pode simplesmente não saber o que fazer com essa operação.
O problema atual: padronizado não significa universalmente suportado
Essa é provavelmente a parte mais importante do artigo.
QUERY já é um método HTTP registrado e padronizado. Isso não significa que sua infraestrutura inteira esteja pronta para ele.
Antes de colocar em produção, eu testaria pelo menos:
cliente HTTP;
browser, se existir consumo direto no front-end;
reverse proxy;
API gateway;
WAF;
CDN;
framework;
ferramenta de observabilidade;
gerador de SDK;
comportamento real de cache.
Um intermediário pode aceitar um método desconhecido e simplesmente encaminhá-lo. Outro pode bloquear a requisição porque QUERY ainda não está em sua allowlist.
E mesmo quando ele encaminha, isso não quer dizer que entenda as novas regras de cache.
Esse é o ponto em que os dois artigos internacionais que usei como leitura chegam praticamente à mesma conclusão por caminhos diferentes: a semântica é boa, mas a adoção deve ser seletiva.
Dave Amit resume o problema olhando para o caminho completo da requisição: cliente, proxy, WAF, CDN e origin precisam se comportar corretamente. A TechTarget chega à mesma recomendação por uma visão mais operacional: começar por endpoints de consulta complexa e tratar a adoção como uma modernização pontual, não como motivo para reescrever APIs que já funcionam.
CORS também merece atenção
QUERY não faz parte dos métodos CORS safelisted.
Portanto, uma chamada cross-origin feita pelo navegador exige preflight.
Na prática, isso não é necessariamente uma mudança dramática para APIs JSON, porque um POST cross-origin com Content-Type: application/json normalmente já exige preflight. Mesmo assim, é mais um item para validar no caminho real da aplicação.
Eu migraria meus POST /search agora?
Não em massa.
Se a API já está em produção, possui vários consumidores e POST /search funciona bem, trocar o método só para ficar semanticamente bonito pode criar mais problema do que resolver.
Eu começaria com QUERY em três cenários:
APIs internas, onde você controla cliente, servidor e infraestrutura.
Novos endpoints de busca complexa, principalmente relatórios, analytics e filtros estruturados.
Ambientes em que retries e cache realmente tragam ganho operacional.
Para uma API pública, eu manteria compatibilidade durante um bom tempo.
Algo assim é totalmente aceitável:
QUERY /products
POST /products/searchOs dois endpoints podem cair no mesmo service interno enquanto clientes antigos continuam utilizando POST.
type ProductSearchInput = {
categories?: string[];
minPrice?: number;
maxPrice?: number;
};
export class ProductSearchService {
async execute(input: ProductSearchInput) {
return repository.search(input);
}
}A regra de negócio não deveria depender do verbo HTTP. É justamente esse desacoplamento que permite evoluir o transporte sem reescrever a busca.
Quando GET continua sendo melhor
Não transforme isso em moda.
Se a sua consulta é:
GET /posts?page=2&category=typescriptGET continua sendo melhor.
É simples, legível, compartilhável, amplamente cacheável e suportado por absolutamente tudo.
QUERY começa a fazer sentido quando o formato da consulta é um objeto de verdade, não quando você possui três parâmetros na URL e quer brincar com o verbo novo.
A regra prática que eu usaria é:
GET: leitura simples e consulta que cabe naturalmente na URI.
QUERY: leitura complexa cujo conteúdo merece um body estruturado.
POST: operação cuja intenção pode criar, disparar ou alterar estado.
O que a RFC 10008 realmente conserta
QUERY não torna sua API automaticamente RESTful, rápida ou segura.
Também não elimina GraphQL, Elasticsearch, JSONPath ou qualquer linguagem de consulta. Ele atua em outra camada.
O que ele resolve é mais básico: finalmente existe uma forma padronizada de dizer, no próprio HTTP:
“Esta requisição carrega uma consulta complexa no body, não pretende alterar o recurso e pode ser repetida com segurança.”
Até agora, a gente normalmente precisava escolher quais dessas informações perder.
Com GET, preservava a semântica da leitura, mas consultas complexas ficavam desconfortáveis na URI.
Com POST, ganhava um body livre, mas o protocolo deixava de saber que aquela operação era uma leitura repetível.
QUERY não é revolucionário porque inventa uma capacidade impossível antes dele. Nós já fazíamos buscas complexas com POST há anos.
Ele é interessante porque transforma uma convenção que só a aplicação conhecia em uma semântica que o restante da stack pode conhecer também.
E isso é exatamente o tipo de mudança de protocolo que parece pequena no código e grande quando começa a chegar em caches, proxies, frameworks, ferramentas de observabilidade e contratos de API.
Eu não migraria tudo amanhã.
Mas, a partir de agora, todo novo POST /alguma-coisa/search merece pelo menos uma pergunta antes de ser criado:
isso é realmente POST ou só estamos usando POST porque, até 2026, faltava um método melhor?
Referências e leituras
RFC 10008 — The HTTP QUERY Method, IETF / RFC Editor.
HTTP Finally Got a QUERY Method, Dave Amit.
A look at HTTP's new QUERY method, TechTarget.