Voltar para o Blog

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.

HTTP QUERY: o que é e como usar com Node.js e TypeScript

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

PropriedadeGETHTTP QUERYPOST
Safesimsimpotencialmente não
Idempotentsimsimpotencialmente não
Body da consultasem semântica definidaesperadoesperado
URI bookmarkável da consultasim (por definição)opcional via Locationnão
Cache como leiturasimsim (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, limit inválido).
  • 406 Not Acceptable — Accept pede 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; GET ali repete aquele resultado.
  • Location: equivalent resource; GET reexecuta 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)

  1. Node.js 22.21.0 (llhttp 9.3.0): req.method === 'QUERY'; fetch e curl -X QUERY OK.
  2. Node.js 20.19.2 (llhttp 9.2.1): body chega, request-line é QUERY ..., mas req.method fica undefined — if (req.method !== 'QUERY') vira 405.
  3. curl 8.14.1 envia QUERY corretamente.
  4. spawnSync(curl) no mesmo processo do servidor deadlocka o event loop — rode o servidor separado.
  5. NestJS 12.4.0 exporta QueryMethod e RequestMethod.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

Precisa implementar isso na sua empresa?

Quer aplicar essas práticas de engenharia, dados ou inteligência artificial no seu produto? Solicite um diagnóstico técnico sem compromisso.

Solicitar orçamento