Skip to content

Node.js

Node.js recognizes QUERY natively since version 21.7.2 (via the llhttp 9.2 update). In practice, treat Node 22 LTS as your reliable baseline. req.method arrives as 'QUERY' — no flags, no monkey-patching.

Here’s everything from http core to Fastify, including Express and the Fetch API.


Minimal server that accepts QUERY /search with a JSON body. Body handling is identical to POST: collect chunks, concatenate, parse.

server.js
import { createServer } from 'node:http';
const server = createServer((req, res) => {
if (req.method === 'QUERY' && req.url === '/search') {
// RFC 10008: Content-Type is mandatory for QUERY
if (!req.headers['content-type']) {
res.writeHead(415, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Content-Type required' }));
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: 'Invalid JSON' }));
return;
}
// Simulate a search based on received filters
const result = {
filters: parsed.filter,
limit: parsed.limit ?? 10,
items: [
{ id: 1, name: 'Mechanical keyboard', price: 89 },
{ id: 2, name: 'Ergonomic mouse', price: 65 },
],
};
// QUERY is safe + idempotent → caching is valid
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=60',
});
res.end(JSON.stringify(result));
});
} else {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
}
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});
{
"filters": { "category": "peripherals", "inStock": true },
"limit": 5,
"items": [
{ "id": 1, "name": "Mechanical keyboard", "price": 89 },
{ "id": 2, "name": "Ergonomic mouse", "price": 65 }
]
}

Using http.request with method: 'QUERY' works out of the box — the parser doesn’t block custom methods.

client.js
import { request } from 'node:http';
const body = JSON.stringify({
filter: { category: 'peripherals', inStock: 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();
Status: 200
{
filters: { category: 'peripherals', inStock: true },
limit: 5,
items: [
{ id: 1, name: 'Mechanical keyboard', price: 89 },
{ id: 2, name: 'Ergonomic mouse', price: 65 }
]
}

Express doesn’t have app.query() — that name is already taken for reading URL query string params. The workaround is app.all() with a method check, or a helper to avoid repeating the boilerplate.

express-server.js
import express from 'express';
const app = express();
app.use(express.json());
// Helper: register a route exclusively for QUERY
function queryRoute(app, path, handler) {
app.all(path, (req, res, next) => {
if (req.method !== 'QUERY') return next();
handler(req, res, next);
});
}
// QUERY /search route
queryRoute(app, '/search', (req, res) => {
// RFC 10008 requires Content-Type
if (!req.headers['content-type']) {
return res.status(415).json({ error: 'Content-Type required' });
}
const { filter, limit = 10, offset = 0 } = req.body;
const result = {
filter,
limit,
offset,
items: [
{ id: 1, name: 'Laptop Pro', price: 1299 },
{ id: 2, name: '4K Monitor', price: 799 },
],
};
// Cache is valid — QUERY is cacheable
res.set('Cache-Control', 'max-age=60');
res.json(result);
});
// Fallback for unsupported methods
app.use((req, res) => {
res.status(405).json({ error: 'Method Not Allowed' });
});
app.listen(3000, () => {
console.log('Express running at http://localhost:3000');
});

app.all() matches a specific route (/search) for any HTTP method. app.use() would also work, but without exact path matching — you’d have to check req.url manually.

CORS: QUERY is not on the safelist (GET/HEAD/POST), so cross-origin requests will trigger a preflight. Add QUERY to Access-Control-Allow-Methods in your CORS middleware.


Fastify supports GET, HEAD, TRACE, DELETE, OPTIONS, PATCH, PUT, and POST by default. For extra methods, use addHttpMethod — it registers the method and creates the route shorthand automatically.

fastify-server.js
import Fastify from 'fastify';
const app = Fastify({ logger: true });
// Register QUERY as a method that accepts a body
app.addHttpMethod('QUERY', { hasBody: true });
// Now app.query() exists as a shorthand
app.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 result = {
filter,
limit,
offset,
items: [
{ id: 1, name: '1TB SSD', price: 119 },
{ id: 2, name: '32GB RAM', price: 189 },
],
};
// Cache headers — QUERY allows caching
reply.header('Cache-Control', 'max-age=60');
return result;
},
});
app.listen({ port: 3000 });
  1. app.addHttpMethod('QUERY', { hasBody: true }) — registers the method in the internal router (find-my-way) and enables body parsing.
  2. After that, app.query(path, opts) works like app.get() or app.post().
  3. Schema validation and serialization work normally — Fastify treats QUERY as a first-class citizen.

Tip: call addHttpMethod before registering routes. If you call it after, the shorthand won’t exist yet.


fetch accepts any string as a method — QUERY is not a “forbidden method” (only CONNECT, TRACE, and TRACK are forbidden per the Fetch Standard).

fetch-client.js
const response = await fetch('http://localhost:3000/search', {
method: 'QUERY',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
filter: { category: 'peripherals', maxPrice: 500 },
limit: 20,
offset: 0,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log(data);
{
"filter": { "category": "peripherals", "maxPrice": 500 },
"limit": 20,
"offset": 0,
"items": [
{ "id": 1, "name": "1TB SSD", "price": 119 },
{ "id": 2, "name": "32GB RAM", "price": 189 }
]
}
  • Duplex streams: if you need to send large bodies as a stream, pass duplex: 'half' in the fetch options.
  • Automatic retry: since QUERY is idempotent, clients can retry on network failure with no risk of side effects.
  • Axios: no .query() shorthand exists. Use axios({ method: 'QUERY', url, headers, data }).

curl accepts any string in -X. The body goes in -d (or --data).

Terminal window
curl -X QUERY http://localhost:3000/search \
-H "Content-Type: application/json" \
-d '{"filter": {"category": "peripherals"}, "limit": 5}'
Terminal window
curl -i -X QUERY http://localhost:3000/search \
-H "Content-Type: application/json" \
-d '{"filter": {"inStock": true}, "limit": 10}'
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
Content-Length: 137
{"filters":{"inStock":true},"limit":10,"items":[{"id":1,"name":"Mechanical keyboard","price":89},{"id":2,"name":"Ergonomic mouse","price":65}]}
Terminal window
curl -i -X QUERY http://localhost:3000/search \
-d '{"filter": {}}'
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json
{"error":"Content-Type required"}
Terminal window
curl -v -X QUERY http://localhost:3000/search \
-H "Content-Type: application/json" \
-d '{"filter": {"active": true}}'

This shows the request line > QUERY /search HTTP/1.1 — confirming the method is being sent correctly.


Runtime/Framework Min version Notes
Node.js 22 LTS Parser recognizes QUERY natively
Express 4.x / 5.x Works via app.all(), no shorthand
Fastify 5.x addHttpMethod('QUERY', { hasBody: true })
fetch (Node) 22+ Works directly — QUERY is not a forbidden method
curl any -X QUERY accepts any method string

  1. Forgetting Content-Type — RFC 10008 requires it. Without it, the server should reject with 415.
  2. Cache key — QUERY is cacheable, but the cache must include the body in its key. Most CDNs don’t do this automatically yet.
  3. CORS preflight — QUERY triggers an OPTIONS preflight on cross-origin requests. Configure Access-Control-Allow-Methods on the server.
  4. Node < 21.7.2 — the parser doesn’t recognize the method and rejects the connection. No workaround — upgrade Node.