HTTP QUERY: o que é e como usar com Node.js e TypeScript
O que é HTTP QUERY na prática: método HTTP safe com body (RFC 10008), GET vs QUERY vs POST, parâmetros em JSON e suporte real em Node.js e NestJS.

Se você pesquisou HTTP QUERY, a resposta curta é esta: trata-se de um método HTTP de leitura que carrega a consulta no body, com semântica safe e idempotent. Em junho de 2026 a IETF publicou a RFC 10008 (The HTTP QUERY Method) como Standards Track. Na prática, HTTP QUERY resolve o meio-termo entre GET (bom para cache e retry, ruim para filtros grandes na URL) e POST /search (bom para JSON no corpo, ruim porque mente sobre efeito colateral).
Antes de misturar conceitos: QUERY não é a query string (?q=foo&limit=10) e não é GraphQL. Query string continua no URI; GraphQL continua sendo linguagem/protocolo, em geral sobre POST. HTTP QUERY é o verbo registrado na IANA Method Registry — um novo método HTTP com regras próprias de Content-Type, cache e descoberta (Allow, Accept-Query, OPTIONS).
Neste guia você vê o que é HTTP QUERY, como ele difere de GET e POST, como configurar servidor em Node.js + TypeScript (puro e com NestJS @QueryMethod), e como enviar parâmetros simples ou filtros JSON complexos com curl e fetch. Os números de versão e os testes abaixo são os mesmos medidos em 6 de novembro de 2026 neste ambiente — em especial a diferença entre Node 20 e Node 22 no parser HTTP.
O que é HTTP QUERY
HTTP QUERY pede que o recurso alvo processe o conteúdo da requisição de forma segura e idempotente e devolva o resultado. O body e o Content-Type definem a consulta; o target URI define o escopo (por exemplo /contacts). Diferente de GET, você não está só pedindo a representação do recurso identificado pela URI: está pedindo uma operação de consulta dentro daquele recurso.
A RFC 10008 deixa explícito que QUERY pode ser repetida ou reiniciada automaticamente sem preocupação com mudanças parciais de estado — exatamente o que falta quando um time usa POST só porque o filtro não cabe na URL.
Por que o método HTTP QUERY existe
GET é safe e idempotent, mas empurra o filtro para o URI. URIs têm limites pouco previsíveis atrás de proxies, WAFs e CDNs; a RFC 9110 recomenda suporte a pelo menos 8000 octetos, o que não cobre predicados aninhados ou SQL. URIs também vazam com mais facilidade para logs, bookmarks e referers.
POST /search com JSON no body resolve o tamanho, mas a pilha HTTP trata POST como potencialmente unsafe e não-idempotent. Cache não assume leitura. Retry automático após queda de conexão fica arriscado — mesmo que a sua busca seja só um SELECT.
HTTP QUERY ocupa esse vão: body como POST, semântica de leitura como GET. A resposta é cacheable, porém a chave de cache deve incorporar o conteúdo da requisição e metadados (não só a URI).
Status normativo: RFC 10008 (não é draft)
Em 6 de novembro de 2026 a fonte primária é a RFC 10008 (Standards Track, published June 2026; RFC Published no datatracker em 15/06/2026). Autores: Julian Reschke, James M. Snell e Mike Bishop. A MDN documenta em QUERY request method. Se um texto antigo citar só o draft draft-ietf-httpbis-safe-method-w-body, atualize a referência para a RFC.
HTTP QUERY vs GET vs POST
| Propriedade | GET | HTTP QUERY | POST |
|---|---|---|---|
| Safe | sim | sim | potencialmente não |
| Idempotent | sim | sim | potencialmente não |
| Body da consulta | sem semântica definida | esperado | esperado |
| URI bookmarkável da consulta | sim (por definição) | opcional via Location | não |
| Cache como leitura | sim | sim (chave inclui body) | só para futuros GET/HEAD |
Safe (RFC 9110 §9.2.1): o cliente não pede nem espera mudança de estado no recurso alvo. Idempotent (§9.2.2): repetir a mesma QUERY não deve produzir efeitos adicionais preocupantes. A query string do URI, se existir, ainda identifica o recurso; se ela altera o resultado é específico da API.
Content-Type, Accept e erros HTTP
Servidores MUST falhar se Content-Type estiver ausente ou inconsistente com o body — sem content sniffing. Status recomendados pela RFC 10008 / MDN:
- 400 — sem media type ou inconsistente com o body.
- 415 Unsupported Media Type — tipo não suportado naquele recurso; anuncie via
Accept-Query. - 422 Unprocessable Content — JSON/SQL ok na forma, mas a consulta não processa (tabela inexistente,
limitinválido). - 406 Not Acceptable —
Acceptpede tipo de resposta impossível. - 405 Method Not Allowed — recurso não aceita QUERY; responda com
Allow. - 501 Not Implemented — servidor (como um todo) não implementa o método; para “este path não fala QUERY”, o caminho típico da RFC/MDN é 405 + Allow.
Accept-Query, Allow, OPTIONS e descoberta
Accept-Query anuncia quais media types o recurso aceita em QUERY. Exemplo da RFC:
Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"
OPTIONS descobre métodos:
OPTIONS /contacts HTTP/1.1
Host: example.org
HTTP/1.1 200 OK
Allow: GET, QUERY, OPTIONS, HEAD
Você também pode enviar QUERY direto: 405 com Allow ensina o contrato. Formatos aceitos aparecem em Accept-Query ou no Accept de um 415.
Location, Content-Location e redirecionamentos
QUERY 2xx pode devolver:
Content-Location: snapshot do resultado;GETali repete aquele resultado.Location: equivalent resource;GETreexecuta a consulta sem reenviar o body (dados podem ter mudado).
Redirecionamentos 301/302/307/308 pedem QUERY similar no novo alvo — a exceção de “virar GET após 301/302” não vale para QUERY. 303 See Other aponta para GET no Location.
Cache, proxies, CDN, WAF e CORS
Respostas a HTTP QUERY são cacheable, mas a chave MUST incluir body e metadados. CDNs/WAFs ainda podem bloquear verbos fora da lista clássica: teste a borda antes de produção. CORS: QUERY não é CORS-safelisted; browser dispara preflight OPTIONS. Reflita QUERY em Access-Control-Allow-Methods. Formulários HTML não disparam QUERY; o caminho típico é fetch().
Limitações reais medidas neste box (06/11/2026)
- Node.js 22.21.0 (llhttp 9.3.0):
req.method === 'QUERY';fetchecurl -X QUERYOK. - Node.js 20.19.2 (llhttp 9.2.1): body chega, request-line é
QUERY ..., masreq.methodficaundefined—if (req.method !== 'QUERY')vira 405. - curl 8.14.1 envia QUERY corretamente.
spawnSync(curl)no mesmo processo do servidor deadlocka o event loop — rode o servidor separado.- NestJS 12.4.0 exporta
QueryMethodeRequestMethod.QUERY; Express 5.3.0 roteia o verbo.
Use Node 22+ para HTTP QUERY em Nest/Express nesta data, até validar o seu 20.x.
Como configurar HTTP QUERY no Node.js
Arquivo: package.json
{
"name": "http-query-lab",
"private": true,
"type": "module",
"engines": { "node": ">=22" }
}
Arquivo: server.mjs (ESM do lab executado; equivalente TS é direto)
import http from 'http';
const CONTACTS = [
{ id: 1, email: '[email protected]', status: 'active', tags: ['vip'] },
{ id: 2, email: '[email protected]', status: 'active', tags: ['br'] },
{ id: 3, email: '[email protected]', status: 'paused', tags: ['vip'] },
];
function send(res, status, headers, obj) {
const body = JSON.stringify(obj);
res.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(body),
Connection: 'close',
...headers,
});
res.end(body);
}
export function createApp() {
return http.createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
if (req.method === 'OPTIONS') {
res.writeHead(204, {
Allow: 'GET, HEAD, OPTIONS, QUERY',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS, QUERY',
'Access-Control-Allow-Headers': 'content-type, accept',
'Content-Length': 0,
Connection: 'close',
});
return res.end();
}
if (req.method !== 'QUERY') {
return send(
res,
405,
{ Allow: 'GET, HEAD, OPTIONS, QUERY' },
{ error: 'method_not_allowed', got: req.method ?? null },
);
}
const ct = req.headers['content-type'];
if (!ct) {
return send(res, 400, { 'Accept-Query': 'application/json' }, { error: 'missing_content_type' });
}
if (!String(ct).toLowerCase().startsWith('application/json')) {
return send(
res,
415,
{ Accept: 'application/json', 'Accept-Query': 'application/json' },
{ error: 'unsupported_media_type' },
);
}
let parsed;
try {
parsed = JSON.parse(raw || '{}');
} catch {
return send(res, 400, {}, { error: 'invalid_json' });
}
const limit = parsed.pagination?.limit ?? parsed.limit;
if (limit !== undefined && (typeof limit !== 'number' || limit < 1 || limit > 100)) {
return send(res, 422, {}, { error: 'unprocessable', detail: 'limit must be number 1..100' });
}
let out = [...CONTACTS];
if (parsed.filter?.status) {
out = out.filter((i) => i.status === parsed.filter.status);
}
if (Array.isArray(parsed.filter?.and)) {
for (const clause of parsed.filter.and) {
if (clause.field === 'status' && clause.op === 'eq') {
out = out.filter((i) => i.status === clause.value);
}
if (clause.field === 'tags' && clause.op === 'contains') {
const want = clause.value || [];
out = out.filter((i) => want.every((t) => i.tags.includes(t)));
}
}
}
const lim = typeof limit === 'number' ? limit : 50;
out = out.slice(0, lim);
if (Array.isArray(parsed.select) && parsed.select.length) {
out = out.map((row) =>
Object.fromEntries(
parsed.select.filter((k) => k in row).map((k) => [k, row[k]]),
),
);
}
return send(
res,
200,
{
Allow: 'GET, HEAD, OPTIONS, QUERY',
'Accept-Query': 'application/json',
Location: '/contacts/stored-queries/demo-1',
'Content-Location': '/contacts/stored-results/demo-1',
Vary: 'Accept, Content-Type',
},
{ ok: true, methodSeen: req.method, echo: parsed, results: out },
);
});
});
}
const app = createApp();
app.listen(3457, '127.0.0.1', () => console.log('listening 3457'));
Configure assim: exija Content-Type, anuncie Allow/Accept-Query, espelhe QUERY no CORS e valide paginação com 422 quando o JSON está ok mas a regra de negócio falha.
Como enviar parâmetros com HTTP QUERY (curl e fetch)
Parâmetros simples — o body carrega o que antes ia na query string:
curl --http1.1 -i -X QUERY http://127.0.0.1:3457/contacts \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
--data-binary '{"filter":{"status":"active"},"limit":10}'
Filtro JSON complexo (AND, contains, select, cursor):
curl --http1.1 -i -X QUERY http://127.0.0.1:3457/contacts \
-H 'Content-Type: application/json' \
--data-binary '{
"select": ["id", "email"],
"filter": {
"and": [
{ "field": "status", "op": "eq", "value": "active" },
{ "field": "tags", "op": "contains", "value": ["vip"] }
]
},
"pagination": { "limit": 20, "cursor": "abc" }
}'
No lab, essa chamada devolveu só o contato id: 1 ([email protected]), com methodSeen: "QUERY", Accept-Query: application/json, Location e Content-Location.
Cliente fetch (Node 22+ / browser):
const res = await fetch('http://127.0.0.1:3457/contacts', {
method: 'QUERY',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
filter: { status: 'active' },
limit: 10,
}),
});
console.log(res.status, res.headers.get('accept-query'));
console.log(await res.json());
Form-urlencoded é válido se o recurso aceitar. Neste lab JSON-only, responde 415:
curl --http1.1 -i -X QUERY http://127.0.0.1:3457/contacts \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'q=foo&limit=10'
Descoberta:
curl --http1.1 -i -X OPTIONS http://127.0.0.1:3457/contacts
curl --http1.1 -i http://127.0.0.1:3457/contacts # GET -> 405 + Allow
HTTP QUERY no NestJS 12.4 com @QueryMethod
No NestJS o decorator não é @Query() (isso é query string). O método HTTP usa @QueryMethod() em @nestjs/common 12.4.0:
/** Note: named QueryMethod (rather than Query) to avoid a name clash */
export declare const QueryMethod: (path?: string | string[]) => MethodDecorator;
Arquivo: src/contacts.controller.ts
import {
Body,
Controller,
Get,
QueryMethod,
Res,
} from '@nestjs/common';
import type { Response } from 'express';
@Controller('contacts')
export class ContactsController {
private readonly items = [
{ id: 1, email: '[email protected]', status: 'active', tags: ['vip'] },
{ id: 2, email: '[email protected]', status: 'active', tags: ['br'] },
{ id: 3, email: '[email protected]', status: 'paused', tags: ['vip'] },
];
@Get()
listHint(@Res({ passthrough: true }) res: Response) {
res.setHeader('Allow', 'GET, HEAD, OPTIONS, QUERY');
res.setHeader('Accept-Query', 'application/json');
return { hint: 'Use QUERY with JSON body for filtered search' };
}
@QueryMethod()
query(@Body() body: Record<string, unknown>, @Res({ passthrough: true }) res: Response) {
const limit = (body as any)?.pagination?.limit ?? (body as any)?.limit;
if (limit !== undefined && (typeof limit !== 'number' || limit < 1 || limit > 100)) {
res.status(422);
return { error: 'unprocessable', detail: 'limit must be 1..100' };
}
let out = [...this.items];
const status = (body as any)?.filter?.status;
if (status) out = out.filter((i) => i.status === status);
out = out.slice(0, typeof limit === 'number' ? limit : 50);
res.setHeader('Allow', 'GET, HEAD, OPTIONS, QUERY');
res.setHeader('Accept-Query', 'application/json');
res.setHeader('Location', '/contacts/stored-queries/demo-1');
res.setHeader('Content-Location', '/contacts/stored-results/demo-1');
res.setHeader('Vary', 'Accept, Content-Type');
return { ok: true, results: out };
}
}
Instale Nest 12.4.0 e rode em Node 22+. Neste box, QUERY /contacts via fetch e curl retornou 200 com Allow e Accept-Query. Confira RequestMethod.QUERY no seu node_modules antes de improvisar @All().
Segurança e logs
HTTP QUERY reduz exposição porque o predicado sai da URI. O benefício some se você embute o filtro sensível em Location/Content-Location, loga o body com PII ou deixa o endpoint aberto — safe não significa público. A RFC recomenda URIs temporárias sem porções sensíveis do conteúdo original.
Quando usar GET, HTTP QUERY ou POST
Use GET quando o filtro cabe no URI e você quer link compartilhável. Use POST quando há criação/alteração de estado, ou a malha ainda não transporta QUERY. Use HTTP QUERY para leitura com input grande, estruturado ou sensível demais para o URI, com retry/cache honestos. Exemplos curtos da RFC são ilustrativos: consulta minúscula ainda favorece GET.
FAQ: HTTP QUERY na prática
O que é HTTP QUERY?
É o método HTTP definido na RFC 10008 para consultas safe e idempotent com parâmetros no body. Em uma frase: leitura com corpo, sem fingir que é POST.
HTTP QUERY é a mesma coisa que query string?
Não. Query string é o componente ?... do URI. QUERY é o verbo da request-line (QUERY /contacts HTTP/1.1). Você pode até ter os dois, mas o conteúdo da consulta em HTTP QUERY vive no body.
Qual a diferença entre GET, QUERY e POST?
GET: leitura pela URI, sem semântica de body. QUERY: leitura com body. POST: potencialmente muda estado; intermediários não assumem retry seguro.
Como enviar os parâmetros em HTTP QUERY?
Serialize o documento de consulta no body (application/json, application/x-www-form-urlencoded, SQL, JSONPath — o que o recurso anunciar em Accept-Query), com Content-Type correto. No cliente: curl -X QUERY ou fetch({ method: 'QUERY', body: ... }).
Node.js e NestJS já suportam HTTP QUERY?
Neste lab, Node 22.21.0 sim (req.method === 'QUERY'). Node 20.19.2 recebe o body mas deixa req.method undefined. NestJS 12.4.0 oferece @QueryMethod(). Sempre valide CDN/WAF/CORS na sua malha.
Conclusão
HTTP QUERY fecha a lacuna de um verbo HTTP de leitura com body. Em novembro de 2026, no ecossistema Node/TypeScript medido aqui, o caminho saudável é Node 22+, Allow/Accept-Query, Content-Type obrigatório e @QueryMethod() de verdade no Nest — não um POST /search disfarçado. Quais dos seus endpoints de busca já são, semanticamente, HTTP QUERY — e o que falta na borda para admitir o verbo sem mentir na pilha?
Referências
- IETF RFC Editor — RFC 10008: The HTTP QUERY Method (Standards Track, June 2026). Consulta em 6 de novembro de 2026.
- RFC Editor info page — https://www.rfc-editor.org/info/rfc10008/
- Datatracker — histórico de draft-ietf-httpbis-safe-method-w-body (RFC Published 15 Jun 2026).
- MDN — QUERY request method. Consulta em 6 de novembro de 2026.
- RFC 9110 — HTTP Semantics (safe/idempotent).
- RFC 9111 — HTTP Caching.
- WHATWG Fetch — https://fetch.spec.whatwg.org (CORS-safelisted methods; citado pela RFC 10008).
- NestJS —
@QueryMethodem@nestjs/[email protected](tipagem local: evita clash com@Query()de query string). - NestJS issue/PR de suporte — Issue #17161, PR #17162.