HTTP QUERY Method in Express and Node.js
How to actually implement the new HTTP QUERY method (RFC 10008) in Express and Node.js, with working code for a search endpoint, CORS, and a POST fallback.
On this page
RFC 10008 standardized the HTTP QUERY method in June 2026, the first new HTTP method since PATCH landed in 2010. QUERY is safe and idempotent like GET, but it carries a request body like POST. That combination finally gives search and filter endpoints a proper home instead of the POST /search workaround nearly every API has leaned on for years.
Plenty has already been written about what QUERY is and why it exists. This post skips that part and goes straight to what actually matters if you are building APIs today: how do you add QUERY support to a real Node.js server right now, given that framework support is still catching up.
If you just want to know whether your specific stack, proxy, or client already understands QUERY before you commit engineering time to it, check the HTTP QUERY Support Matrix for a live, filterable compatibility reference instead of hunting through GitHub issues yourself.
Current Framework Support Status
This table reflects support status as of this article's publish date. Framework support for a brand-new HTTP method evolves quickly, so verify current status before relying on this for a production decision. The HTTP QUERY Support Matrix tracks this across frameworks, proxies, and clients on an ongoing basis, with a source link for every row.
| Layer | Status | Notes |
|---|---|---|
| Node.js core (node:http) | Supported | Landed in Node 21.7.2, tracked in nodejs/node#51562 |
| Express | Manual workaround required | Router does not yet expose app.query(), tracked in expressjs/express#5615, open as of publish |
| Fastify | Check current release notes | Verify against the Fastify changelog before depending on native support |
| Hono | Check current release notes | Verify against Hono's routing documentation |
| Browsers (fetch) | Inconsistent | Check MDN's current compatibility table before relying on client-side fetch(method: 'QUERY') |
The practical takeaway: Node.js itself already understands QUERY as a method. Getting your web framework's router to dispatch QUERY requests to the right handler is the part that needs manual work right now.
Adding QUERY Support with the Raw Node.js http Module
Since Node core already parses QUERY as a valid method, a raw node:http server handles it without any extra configuration. This is worth seeing first because it proves the method itself works end to end before you deal with framework-level routing.
const http = require('node:http');
const server = http.createServer((req, res) => {
if (req.method === 'QUERY') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
const filters = JSON.parse(body);
console.log('QUERY request received with filters:', filters);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ results: [], filters }));
});
return;
}
res.writeHead(404);
res.end();
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});Test it directly with curl to confirm your Node.js version handles it correctly:
curl -X QUERY http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{"category":"electronics","priceMax":500}'If you get back a JSON response instead of a connection error or a 405, both your Node.js version and this basic handling pattern work as expected. Everything else in this article builds on top of that baseline.
Adding QUERY Support to Express (The Practical Workaround)
Express's router does not currently expose a first-class app.query() method the way it exposes app.get() or app.post(). Until that lands, the practical approach is a small piece of middleware that intercepts QUERY requests before they hit Express's method-based routing and dispatches them manually.
const express = require('express');
const app = express();
app.use(express.json());
// Manual QUERY method routing middleware
// Place this before your other route definitions
const queryRoutes = new Map();
function query(path, handler) {
queryRoutes.set(path, handler);
}
app.use((req, res, next) => {
if (req.method === 'QUERY') {
const handler = queryRoutes.get(req.path);
if (handler) {
return handler(req, res, next);
}
return res.status(404).json({ error: 'No QUERY handler for this path' });
}
next();
});
// Register a QUERY route the same way you'd register app.get()
query('/products', (req, res) => {
const filters = req.body;
console.log('Searching products with filters:', filters);
res.json({ results: [], appliedFilters: filters });
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});This pattern is intentionally minimal so it is easy to adapt. If you need path parameters, like /products/:category, bring in a small path-matching library, such as path-to-regexp, which Express itself uses internally, rather than the plain Map lookup shown here. For a broader look at how modern routers structure middleware and validation around a request, this walkthrough of building REST APIs with Hono's routing patterns covers a framework that takes a more explicit, method-driven approach than Express.
A Complete Working Example: Search Endpoint
Here is the exact use case RFC 10008 was designed for: a product search endpoint with a structured filter body, replacing the common POST /products/search anti-pattern.
const express = require('express');
const app = express();
app.use(express.json());
const products = [
{ id: 1, name: 'Laptop', category: 'electronics', price: 899 },
{ id: 2, name: 'Desk Chair', category: 'furniture', price: 249 },
{ id: 3, name: 'Headphones', category: 'electronics', price: 179 },
{ id: 4, name: 'Monitor', category: 'electronics', price: 329 },
];
const queryRoutes = new Map();
function query(path, handler) {
queryRoutes.set(path, handler);
}
app.use((req, res, next) => {
if (req.method === 'QUERY') {
const handler = queryRoutes.get(req.path);
if (handler) return handler(req, res, next);
return res.status(404).json({ error: 'Not found' });
}
next();
});
query('/products', (req, res) => {
const { category, priceMax, priceMin, sort } = req.body;
let results = products;
if (category) {
results = results.filter((p) => p.category === category);
}
if (priceMax !== undefined) {
results = results.filter((p) => p.price <= priceMax);
}
if (priceMin !== undefined) {
results = results.filter((p) => p.price >= priceMin);
}
if (sort === 'price_asc') {
results = [...results].sort((a, b) => a.price - b.price);
} else if (sort === 'price_desc') {
results = [...results].sort((a, b) => b.price - a.price);
}
res.json({ count: results.length, results });
});
app.listen(3000, () => console.log('Server on port 3000'));Test the full filtering behavior:
curl -X QUERY http://localhost:3000/products \
-H "Content-Type: application/json" \
-d '{"category":"electronics","priceMax":400,"sort":"price_asc"}'Compare this to what the same request looked like as a POST-based workaround before QUERY existed. Functionally identical, but now the method itself communicates "this is safe, idempotent, and cacheable" to any intermediary that understands it, without any documentation needed. That distinction matters more than it looks: a GET or QUERY request can be cached, retried automatically, and prefetched by a client without side effects, while a POST request cannot be assumed safe to retry blindly. If caching semantics on read-heavy endpoints are new territory for your team, this breakdown of caching strategies covers the tradeoffs QUERY's cacheable-but-body-keyed nature runs into in practice.
Handling CORS Preflight for QUERY
QUERY is not part of the CORS-safelisted method set, which only covers GET, HEAD, and POST with specific content types. This means any cross-origin QUERY request triggers a browser preflight OPTIONS request first. If your API is called from a different origin than where it is hosted, you need to handle this explicitly.
const cors = require('cors');
app.use(cors({
origin: 'https://your-frontend-domain.com',
methods: ['GET', 'POST', 'QUERY', 'OPTIONS'], // QUERY must be explicit here
allowedHeaders: ['Content-Type'],
}));Testing QUERY Requests
With curl, the -X flag sets the method directly, and QUERY works like any other method:
curl -X QUERY http://localhost:3000/products \
-H "Content-Type: application/json" \
-d '{"category":"electronics"}'With fetch() in the browser or Node.js, check current support before relying on it in production. As of this writing, support is inconsistent across environments:
// This may or may not work depending on your runtime's fetch implementation
// Verify against current browser/Node.js compatibility before shipping
const response = await fetch('http://localhost:3000/products', {
method: 'QUERY',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category: 'electronics' }),
});If fetch() with method: 'QUERY' throws or silently fails in your target environment, you are not doing anything wrong. Client-side support is genuinely still catching up to the server-side story. This is exactly why the fallback pattern in the next section matters.
With Postman or Insomnia, most modern versions accept custom or recently added HTTP methods in the method dropdown, or let you type a custom method name directly. Check your specific client version if the dropdown does not list QUERY yet. Once you have an endpoint working locally, wiring a QUERY-based request into your existing CI checks is no different from any other route: if you already run integration tests against pull requests, this CI/CD walkthrough covers testing new API endpoints as part of your pipeline before they reach production.
Fallback Pattern for Clients
Because QUERY support varies across proxies, older HTTP clients, and some browser environments, a defensive client-side pattern attempts QUERY first and falls back to the POST workaround if the server or an intermediary does not support it.
async function searchProducts(filters) {
try {
const response = await fetch('/products', {
method: 'QUERY',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(filters),
});
if (response.status === 405 || response.status === 501) {
// Method not supported by this server or an intermediary
throw new Error('QUERY not supported, falling back');
}
return response.json();
} catch (err) {
// Fall back to the POST-based search endpoint
const response = await fetch('/products/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(filters),
});
return response.json();
}
}On the server side, this means maintaining both a QUERY handler and a POST fallback handler pointing at the same underlying search logic during the transition period, until QUERY support is reliably present across your entire request path: your client, any proxies or CDNs in between, and your server framework.
// Shared handler logic, called from both QUERY and POST fallback routes
function handleProductSearch(req, res) {
const { category, priceMax, priceMin, sort } = req.body;
// ... same filtering logic as before
res.json({ count: results.length, results });
}
query('/products', handleProductSearch);
app.post('/products/search', handleProductSearch); // fallback routeThis dual-route approach is the pragmatic path most teams are taking right now: add QUERY support where you can, keep the POST fallback available, and retire the fallback once support has genuinely stabilized across your stack. Since that stabilization is a moving target, it is worth periodically re-checking the current compatibility status across your frameworks, proxies, and clients rather than assuming today's manual workaround is still necessary six months from now.
Common Pitfalls When Adopting QUERY
A handful of mistakes show up repeatedly in early QUERY implementations, mostly because teams treat it as a drop-in replacement for POST rather than a method with its own specific contract. Being aware of these before you ship saves a round of confused bug reports later.
- Assuming every intermediary passes it through unmodified: older reverse proxies, some corporate firewalls, and certain API gateways only allow a fixed allowlist of HTTP methods. A QUERY request can be silently dropped or rewritten to GET before it ever reaches your server, which looks identical to a routing bug on your own code. Test through your actual production network path, not just localhost.
- Skipping body size limits: because QUERY looks like GET, it is easy to forget that it carries a body the same way POST does. Apply the same request body size limits you already enforce on POST endpoints (express.json({ limit: '100kb' }), for example), since a QUERY endpoint accepts arbitrary JSON from a client exactly like any other body-carrying method.
- Not validating that the body is actually JSON: a malformed or missing Content-Type header on a QUERY request will produce the same parsing failures you would see on a broken POST request. Validate and return a clear 400 response rather than letting an unhandled JSON.parse exception crash the handler.
- Overtrusting the safe and idempotent guarantee: QUERY being specified as safe and idempotent describes intended semantics, not an enforced runtime guarantee. Nothing stops a handler registered on a QUERY route from writing to a database. Treat the safety guarantee as a contract you are responsible for upholding in your own handler code, the same way GET has always relied on developer discipline rather than framework enforcement.
- Caching without a body-aware cache key: a caching layer that keys purely on URL path, the way it might for a simple GET, will return the wrong cached response for two QUERY requests to the same path with different filter bodies. Any cache in front of a QUERY endpoint needs to incorporate the request body into its cache key, not just the path and query string.
Frequently Asked Questions
What is the HTTP QUERY method?
QUERY is an HTTP method standardized by RFC 10008 in June 2026. It combines GET's safe and idempotent semantics with POST's ability to carry a structured request body, giving search and filter operations a method that accurately describes their intent instead of overloading POST or GET.
Does Express support the HTTP QUERY method natively?
Not yet as a first-class router method. Express does not currently expose app.query() the way it exposes app.get() or app.post(), and the tracking issue (expressjs/express#5615) remains open as of this article's publish date. The workaround is a small piece of middleware that intercepts requests where req.method === 'QUERY' and dispatches them to registered handlers manually, shown in the Express section above.
How is HTTP QUERY different from POST for a search endpoint?
| Property | POST /search | QUERY |
|---|---|---|
| Safe (no side effects) | Not guaranteed by the method itself | Yes, by specification |
| Idempotent | Not guaranteed | Yes, by specification |
| Cacheable by intermediaries | No | Yes, in principle |
| Carries a request body | Yes | Yes |
| Communicates intent in HTTP semantics | No, relies on the URL path convention | Yes, the method itself signals read-only intent |
Both can send the same JSON filter body and both work today. The difference is semantic: QUERY tells proxies, caches, and other tooling that the request is safe to retry and potentially cache, without needing a code comment or API doc to explain the convention.
Does Node.js support the HTTP QUERY method out of the box?
Yes. Node.js core added QUERY method parsing in Node 21.7.2, tracked in nodejs/node#51562. A raw node:http server handles req.method === 'QUERY' without any extra configuration, as shown in the raw HTTP example above. The gap is entirely at the framework routing layer, not in Node itself.
Why does my QUERY request fail from the browser but work from curl?
This is almost always a CORS preflight issue. QUERY is not in the CORS-safelisted method set, so any cross-origin request triggers a browser preflight OPTIONS check first. If your CORS configuration's methods array does not explicitly list QUERY, the preflight fails and the browser blocks the request, even though the same request succeeds from curl or a server-to-server call that never goes through CORS. See the CORS section above for the exact configuration fix.
Should I switch my search endpoints to QUERY today?
It depends on your full request path, not just your server. Node.js core is ready, but you also need to account for your framework's router, any reverse proxy or CDN in front of your API, and the HTTP clients that will call it, including browser fetch() support, which is still inconsistent.
- Safe to start now: server-to-server APIs where you control both ends, since curl-style and most backend HTTP clients already support custom methods correctly
- Add a fallback first: public-facing browser APIs, since fetch() support varies and a cross-origin CORS misconfiguration will silently break requests
- Check before committing: your specific CDN or reverse proxy, since some older proxy configurations reject unrecognized methods outright
The dual-route pattern in the fallback section above lets you adopt QUERY today while keeping a POST-based safety net until your entire stack has caught up.
Related Articles
Hono.js Tutorial: REST API with Zod, JWT & Cloudflare Workers
Step-by-step Hono.js tutorial: routing, middleware, Zod validation, JWT auth, and deployment to Cloudflare Workers and Node.js. Working code throughout.
GitHub Actions Tutorial: CI/CD from Push to Deploy (2026)
Learn GitHub Actions: write your first workflow, run tests automatically, use secrets safely, deploy via SSH, cache dependencies, and run matrix builds.
Caching Strategies Explained: CDN, Redis & DB Cache
A practical guide to caching strategies: browser cache, CDN, in-process memory, and Redis. Learn which layer to use, cache-aside patterns, and invalidation.