query-http-demo

Mismo endpoint con GET, POST y el nuevo método HTTP QUERY (RFC 10008) en Express

35
5
35
HTML
public

QUERY · el nuevo método HTTP (RFC 10008)

Demo en Express del mismo endpoint servido con GET, POST y el nuevo
método QUERY, para ver de un vistazo qué aporta y por qué importa la caché.

QUERY acaba de pasar a Proposed Standard. La idea en una frase:

QUERY = la seguridad y la cacheabilidad de GET + la capacidad de mandar un body de POST.

  • ✅ Como GET: es un método seguro (no modifica el recurso) → la respuesta se puede cachear.
  • ✅ Como POST: puede llevar body, así que mandas un JSON con filtros complejos.
  • ✅ Lo que ninguno de los dos te da solo: mandar un JSON Y poder cachear la respuesta.

Enlaces:


El problema: GET vs POST para filtrar datos

Imagina un buscador de productos con filtros: categorías, rango de precio, rating, orden…

Opción A — GET con query string

GET /api/products/search?categories=laptop,phone&priceMin=800&priceMax=1500&minRating=4.5&sortBy=price&sortOrder=desc
  • 👍 Es cacheable por defecto (navegador, CDN, proxies).
  • 👎 Todo es texto: hay que castear números y booleanos a mano en el server.
  • 👎 Los filtros complejos o anidados (un array de objetos, condiciones OR/AND) no caben bien en una URL.
  • 👎 Límite de longitud de URL y los parámetros acaban en logs (peor para datos sensibles).

¿Cuánto puede llegar a medir una URL en GET? (límites reales)

No hay un límite único universal.

A nivel de estándar HTTP, no se define una longitud máxima estricta para una URI. Lo que sí recomienda RFC 9110 es que clientes y servidores soporten al menos 8000 octetos en URIs dentro de elementos del protocolo.

En la práctica:

Contexto Límite recomendable
Máxima compatibilidad ≤ 2.000 caracteres
Apps modernas controladas ≤ 8.000 bytes aprox.
Sitemap SEO ≤ 2.048 caracteres por URL
Apache por defecto 8190 bytes en la request line
Nginx por defecto alrededor de 8K para la request line, según buffer

Mi recomendación práctica:

  • URL pública, compartible o SEO: < 2.000 caracteres
  • API interna o app moderna: intenta no pasar de 8 KB
  • Si necesitas más datos: usa POST con body, no GET con query params

Especialmente si estás metiendo filtros, JSON, tokens o estado de la app en la URL, mejor evitar URLs largas. Pueden fallar en proxies, servidores, WAFs, navegadores, analytics, emails o herramientas intermedias.

Más problemas de filtrar con GET (query params)

Tradicionalmente, si querías filtrar un recurso, usabas query params en una petición GET
(p. ej. /api/v1/users?role=admin&status=active&sort=desc). Funciona bien para filtros
simples
, pero en cuanto necesitas consultas relacionales complejas, anidamiento
profundo
o lógica avanzada, la URL se vuelve enorme, difícil de leer y a veces choca
contra los límites de caracteres del navegador o del servidor.

Otros problemas potenciales:

  • Caracteres especiales o no ASCII: hay que codificarlos (encodeURIComponent), lo que
    aumenta el tamaño de la petición.
  • Logs: servidores y middlewares suelen registrar los parámetros de la petición, lo que
    puede ser un problema con datos sensibles.
  • Arrays mal definidos: expresar un array no está estandarizado y depende de la
    implementación: ?roles[0]=admin&roles[1]=reporter vs ?roles=admin&roles=reporter vs
    ?roles[]=admin&roles[]=reporter.
  • Estructuras profundamente anidadas: mismo problema, no hay una forma canónica de
    representarlas en la query string.

Y entonces, ¿por qué no mandar simplemente un GET con body JSON? En teoría debería
funcionar: ningún RFC de HTTP prohíbe explícitamente un body en un GET, pero sí indican
que no debería hacerse
. Como consecuencia, cada cliente, proxy y servidor lo trata de forma
distinta: algunos lo rechazan, otros descartan el body y otros lo interpretan.

Por eso, usar GET con body es mala idea: por ejemplo, usuarios detrás del firewall
corporativo o con otro navegador podrían no poder usar tu web. Es también la razón de que
no haya un RFC nuevo que diga que GET ahora admite body: rompería muchísimas
implementaciones existentes. Justo el hueco que viene a llenar QUERY.

Opción B — POST con body

POST /api/products/search
Content-Type: application/json

{ "categories": ["laptop","phone"], "price": { "min": 800, "max": 1500 }, "minRating": 4.5 }
  • 👍 Body JSON rico: tipos reales, objetos anidados, lo que quieras.
  • 👎 No es cacheable. POST se define como método no seguro (puede cambiar estado),
    así que navegadores y CDNs no cachean su respuesta → cada búsqueda golpea la BBDD.
  • 👎 Semánticamente es mentira: no estás creando nada, solo leyendo.

Llevábamos años haciendo “buscar con POST” sabiendo que estaba mal. Por eso nace QUERY.

Opción C — QUERY 🎉

QUERY /api/products/search
Content-Type: application/json

{ "categories": ["laptop","phone"], "price": { "min": 800, "max": 1500 }, "minRating": 4.5 }
  • 👍 Body JSON rico, igual que POST.
  • 👍 Es segurocacheable, igual que GET. La clave de caché es el contenido (el body).
  • 👍 Semántica correcta: “consulto” datos, no los modifico.

Cómo lo soporta Express

No hace falta nada raro. En Node 26 el parser HTTP ya incluye QUERY:

require('http').METHODS.includes('QUERY') // true

Y como Express 5 genera un método por cada verbo de http.METHODS, te expone
app.query() directamente (aunque aún no esté documentado, ver issue #5615):

app.query('/api/products/search', (req, res) => {
  const filtros = req.body          // express.json() parsea el body igual que en POST
  // ...filtrar y responder con Cache-Control
})

express.json() mira el Content-Type, no el método, así que parsea el body
de QUERY exactamente igual que el de POST. Cero configuración extra.


Estructura del proyecto

query-demo/
├── server.js          # Express: MISMO endpoint con GET, POST y QUERY
├── search.js          # Filtro compartido + caché en memoria (TTL 30s y delay simulado)
├── data/products.js   # Catálogo de ejemplo
├── public/index.html  # UI en el navegador: 3 verbos + petición HTTP en crudo + cURL/fetch
│                       # (tipografía Geist Sans/Mono/Pixel servida desde el paquete)
├── requests.http      # Peticiones listas para la extensión REST Client de VS Code
└── demo.sh            # Demo con curl

El endpoint /api/products/search está implementado tres veces con la misma
lógica de filtrado; lo único que cambia es de dónde salen los filtros y la
política de caché:

Verbo Filtros desde Cache-Control Caché
GET query string public, max-age=30 ✅ HIT/MISS
POST body JSON no-store ❌ siempre golpea la BBDD
QUERY body JSON public, max-age=30 ✅ HIT/MISS

Para que la caché se note, search.js simula una BBDD lenta (300 ms) y cachea
en memoria con un TTL de 30 s (el mismo valor que el max-age=30 del header, así
el HIT/MISS es coherente: pasados 30 s la entrada caduca y vuelve a ser MISS). La
respuesta incluye headers visibles:

  • X-Cache: HIT | MISS
  • Server-Timing: db;dur=<ms> (0 ms cuando viene de caché)
  • X-Filter-Source: querystring | body

Cómo ejecutarlo

npm install        # o pnpm install
npm start          # http://localhost:3000   (o npm run dev para --watch)

Y en otra terminal:

npm run demo       # lanza demo.sh con curl

También puedes abrir http://localhost:3000 para la UI, o usar requests.http
con la extensión REST Client de VS Code.

Probarlo a mano con curl

# GET — filtros en la URL (cacheable)
curl -i "http://localhost:3000/api/products/search?categories=laptop&priceMax=1200&minRating=4.5"

# POST — body JSON, pero Cache-Control: no-store
curl -i -X POST http://localhost:3000/api/products/search \
  -H 'Content-Type: application/json' \
  -d '{"categories":["tablet"],"price":{"max":1200}}'

# QUERY — mismo body que POST, pero cacheable
curl -i -X QUERY http://localhost:3000/api/products/search \
  -H 'Content-Type: application/json' \
  -d '{"categories":["phone"],"price":{"min":500,"max":1300},"minRating":4.5}'

⚠️ Estado del soporte (importante para no engañar a nadie)

  • Node 26 / Express 5: ✅ el servidor entiende QUERY de forma nativa.
  • curl: ✅ -X QUERY funciona perfecto.
  • Navegadores (fetch): ⚠️ el soporte está llegando, no es universal. Por eso
    el botón QUERY de la UI está envuelto en un try/catch que avisa si tu navegador
    aún no lo permite. Además, la caché HTTP del propio navegador hoy solo guarda
    GET/HEAD: el HIT que ves en la demo es la caché del servidor (X-Cache).
  • CDNs / proxies: ⚠️ irán añadiendo soporte para cachear QUERY por método + body.

Es decir: la especificación ya está, y el lado servidor funciona hoy; el
ecosistema (navegadores, CDNs) está poniéndose al día. Buen momento para contarlo. 🙂


Licencia

MIT

v0.3.3[beta]