Node.js
Node.js reconhece QUERY nativamente desde a versão 21.7.2 (via atualização do llhttp 9.2). Na prática, trate o Node 22 LTS como baseline confiável. req.method já chega como 'QUERY' — sem flags, sem monkey-patch.
Aqui vai do http core até Fastify, passando por Express e a Fetch API.
Node.js http core — Servidor
Seção intitulada “Node.js http core — Servidor”Servidor mínimo que aceita QUERY /search com body JSON. O tratamento do body é idêntico ao que você já faz com POST: coleta chunks, concatena, parseia.
import { createServer } from 'node:http';
const server = createServer((req, res) => { if (req.method === 'QUERY' && req.url === '/search') { // RFC 10008: Content-Type é obrigatório em QUERY if (!req.headers['content-type']) { res.writeHead(415, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Content-Type obrigatório' })); return; }
let body = ''; req.on('data', (chunk) => { body += chunk; });
req.on('end', () => { let parsed; try { parsed = JSON.parse(body); } catch { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'JSON inválido' })); return; }
// Simula busca com base nos filtros recebidos const resultado = { filtros: parsed.filter, limit: parsed.limit ?? 10, items: [ { id: 1, nome: 'Teclado mecânico', preco: 450 }, { id: 2, nome: 'Mouse ergonômico', preco: 280 }, ], };
// QUERY é safe + idempotent → caching é válido res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60', }); res.end(JSON.stringify(resultado)); }); } else { res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Method Not Allowed' })); }});
server.listen(3000, () => { console.log('Servidor rodando em http://localhost:3000');});Output esperado (resposta ao curl)
Seção intitulada “Output esperado (resposta ao curl)”{ "filtros": { "categoria": "perifericos", "emEstoque": true }, "limit": 5, "items": [ { "id": 1, "nome": "Teclado mecânico", "preco": 450 }, { "id": 2, "nome": "Mouse ergonômico", "preco": 280 } ]}Node.js http core — Cliente
Seção intitulada “Node.js http core — Cliente”Usar http.request com method: 'QUERY' funciona direto — o parser não bloqueia métodos custom.
import { request } from 'node:http';
const body = JSON.stringify({ filter: { categoria: 'perifericos', emEstoque: true }, limit: 5,});
const req = request( { hostname: 'localhost', port: 3000, path: '/search', method: 'QUERY', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), }, }, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { console.log(`Status: ${res.statusCode}`); console.log(JSON.parse(data)); }); });
req.write(body);req.end();Output esperado
Seção intitulada “Output esperado”Status: 200{ filtros: { categoria: 'perifericos', emEstoque: true }, limit: 5, items: [ { id: 1, nome: 'Teclado mecânico', preco: 450 }, { id: 2, nome: 'Mouse ergonômico', preco: 280 } ]}Express
Seção intitulada “Express”Express não tem app.query() — esse nome já é usado pra acessar query string params. A saída é app.all() com checagem de método, ou um helper pra não repetir o boilerplate.
import express from 'express';
const app = express();app.use(express.json());
// Helper: registra rota exclusiva pra QUERYfunction queryRoute(app, path, handler) { app.all(path, (req, res, next) => { if (req.method !== 'QUERY') return next(); handler(req, res, next); });}
// Rota QUERY /searchqueryRoute(app, '/search', (req, res) => { // RFC 10008 exige Content-Type if (!req.headers['content-type']) { return res.status(415).json({ error: 'Content-Type obrigatório' }); }
const { filter, limit = 10, offset = 0 } = req.body;
const resultado = { filter, limit, offset, items: [ { id: 1, nome: 'Notebook Pro', preco: 5200 }, { id: 2, nome: 'Monitor 4K', preco: 3100 }, ], };
// Cache válido — QUERY é cacheable res.set('Cache-Control', 'max-age=60'); res.json(resultado);});
// Fallback pra métodos não suportadosapp.use((req, res) => { res.status(405).json({ error: 'Method Not Allowed' });});
app.listen(3000, () => { console.log('Express rodando em http://localhost:3000');});Por que app.all() e não app.use()
Seção intitulada “Por que app.all() e não app.use()”app.all() casa com a rota específica (/search) para qualquer método HTTP. O app.use() também funcionaria, mas sem matching de path exato — teria que checar req.url manualmente.
CORS: QUERY não está na safelist (GET/HEAD/POST), então requisições cross-origin vão disparar preflight. Adicione
QUERYnoAccess-Control-Allow-Methodsdo seu middleware de CORS.
Fastify (addHttpMethod)
Seção intitulada “Fastify (addHttpMethod)”Fastify suporta GET, HEAD, TRACE, DELETE, OPTIONS, PATCH, PUT e POST por padrão. Pra métodos extras, use addHttpMethod — ele registra o método e cria o shorthand automaticamente.
import Fastify from 'fastify';
const app = Fastify({ logger: true });
// Registra QUERY como método que aceita bodyapp.addHttpMethod('QUERY', { hasBody: true });
// Agora app.query() existe como shorthandapp.query('/search', { schema: { body: { type: 'object', properties: { filter: { type: 'object' }, limit: { type: 'integer', default: 10 }, offset: { type: 'integer', default: 0 }, }, }, response: { 200: { type: 'object', properties: { filter: { type: 'object' }, limit: { type: 'integer' }, offset: { type: 'integer' }, items: { type: 'array' }, }, }, }, }, handler: async (request, reply) => { const { filter, limit, offset } = request.body;
const resultado = { filter, limit, offset, items: [ { id: 1, nome: 'SSD 1TB', preco: 620 }, { id: 2, nome: 'RAM 32GB', preco: 890 }, ], };
// Cache headers — QUERY permite caching reply.header('Cache-Control', 'max-age=60'); return resultado; },});
app.listen({ port: 3000 });Como funciona o addHttpMethod
Seção intitulada “Como funciona o addHttpMethod”app.addHttpMethod('QUERY', { hasBody: true })— registra o método no router interno (find-my-way) e habilita parsing de body.- Depois disso,
app.query(path, opts)funciona comoapp.get()ouapp.post(). - Schema validation e serialization funcionam normalmente — Fastify trata QUERY como cidadão de primeira classe.
Dica: chame
addHttpMethodantes de registrar rotas. Se chamar depois, o shorthand não vai existir ainda.
Fetch API (Node 22+)
Seção intitulada “Fetch API (Node 22+)”fetch aceita qualquer string como método — QUERY não é um “forbidden method” (só CONNECT, TRACE e TRACK são proibidos pelo Fetch Standard).
const response = await fetch('http://localhost:3000/search', { method: 'QUERY', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ filter: { categoria: 'perifericos', precoMax: 500 }, limit: 20, offset: 0, }),});
if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`);}
const data = await response.json();console.log(data);Output esperado
Seção intitulada “Output esperado”{ "filter": { "categoria": "perifericos", "precoMax": 500 }, "limit": 20, "offset": 0, "items": [ { "id": 1, "nome": "SSD 1TB", "preco": 620 }, { "id": 2, "nome": "RAM 32GB", "preco": 890 } ]}Notas sobre fetch + QUERY
Seção intitulada “Notas sobre fetch + QUERY”- Duplex streams: se precisar enviar bodies grandes como stream, passe
duplex: 'half'nas opções do fetch. - Retry automático: como QUERY é idempotent, clientes podem retentar em caso de falha de rede sem risco de efeito colateral.
- Axios: não tem
.query()shorthand. Useaxios({ method: 'QUERY', url, headers, data }).
Testando com curl
Seção intitulada “Testando com curl”curl aceita qualquer string em -X. O body vai em -d (ou --data).
Requisição básica
Seção intitulada “Requisição básica”curl -X QUERY http://localhost:3000/search \ -H "Content-Type: application/json" \ -d '{"filter": {"categoria": "perifericos"}, "limit": 5}'Com headers de resposta visíveis
Seção intitulada “Com headers de resposta visíveis”curl -i -X QUERY http://localhost:3000/search \ -H "Content-Type: application/json" \ -d '{"filter": {"emEstoque": true}, "limit": 10}'Output esperado
Seção intitulada “Output esperado”HTTP/1.1 200 OKContent-Type: application/jsonCache-Control: max-age=60Content-Length: 142
{"filtros":{"emEstoque":true},"limit":10,"items":[{"id":1,"nome":"Teclado mecânico","preco":450},{"id":2,"nome":"Mouse ergonômico","preco":280}]}Testando erro sem Content-Type
Seção intitulada “Testando erro sem Content-Type”curl -i -X QUERY http://localhost:3000/search \ -d '{"filter": {}}'HTTP/1.1 415 Unsupported Media TypeContent-Type: application/json
{"error":"Content-Type obrigatório"}Com verbose pra debug
Seção intitulada “Com verbose pra debug”curl -v -X QUERY http://localhost:3000/search \ -H "Content-Type: application/json" \ -d '{"filter": {"ativo": true}}'Isso mostra a request line > QUERY /search HTTP/1.1 — confirmação de que o método está sendo enviado corretamente.
Versões mínimas
Seção intitulada “Versões mínimas”| Runtime/Framework | Versão mínima | Notas |
|---|---|---|
| Node.js | 22 LTS | Parser reconhece QUERY nativamente |
| Express | 4.x / 5.x | Funciona via app.all(), sem shorthand |
| Fastify | 5.x | addHttpMethod('QUERY', { hasBody: true }) |
| fetch (Node) | 22+ | Funciona direto — QUERY não é forbidden method |
| curl | qualquer | -X QUERY aceita qualquer string de método |
Armadilhas comuns
Seção intitulada “Armadilhas comuns”- Esquecer Content-Type — a RFC 10008 exige. Sem ele, o servidor deve rejeitar com 415.
- Cache key — QUERY é cacheable, mas o cache precisa incluir o body na chave. A maioria dos CDNs ainda não faz isso automaticamente.
- CORS preflight — QUERY dispara OPTIONS preflight em requisições cross-origin. Configure
Access-Control-Allow-Methodsno servidor. - Node < 21.7.2 — o parser não reconhece o método e rejeita a conexão. Não tem workaround — atualize o Node.