Skip to content

Before & After

Before: tangled POST and overflowing GET URL. After: clean QUERY with body.

Code speaks louder than prose. Each block below produces the same response — the difference is what you’re telling HTTP infrastructure.


POST /api/employees/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"department": "engineering",
"skills": ["typescript", "rust"],
"experience_min": 3,
"location": "remote",
"active": true
}
QUERY /api/employees HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"department": "engineering",
"skills": ["typescript", "rust"],
"experience_min": 3,
"location": "remote",
"active": true
}

Server response is identical. What changes is the semantic contract.


POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"query": "query GetUser($id: ID!) { user(id: $id) { name email avatar posts { title } } }",
"variables": { "id": "usr_42" }
}

✅ After: QUERY for reads, POST for mutations

Section titled “✅ After: QUERY for reads, POST for mutations”
QUERY /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"query": "query GetUser($id: ID!) { user(id: $id) { name email avatar posts { title } } }",
"variables": { "id": "usr_42" }
}

GraphQL is the biggest immediate beneficiary. Queries can finally be cached at the HTTP layer — no hacks needed.


POST /products/_search HTTP/1.1
Host: elasticsearch.internal:9200
Content-Type: application/json
{
"query": {
"bool": {
"must": [
{ "match": { "category": "electronics" } },
{ "range": { "price": { "gte": 100, "lte": 500 } } }
],
"filter": [
{ "term": { "in_stock": true } }
]
}
},
"sort": [{ "price": "asc" }],
"size": 20
}
QUERY /products/_search HTTP/1.1
Host: elasticsearch.internal:9200
Content-Type: application/json
{
"query": {
"bool": {
"must": [
{ "match": { "category": "electronics" } },
{ "range": { "price": { "gte": 100, "lte": 500 } } }
],
"filter": [
{ "term": { "in_stock": true } }
]
}
},
"sort": [{ "price": "asc" }],
"size": 20
}

Think monitoring dashboards polling every 5s: identical QUERY responses can be served from cache.


❌ Before: GET with enormous query string

Section titled “❌ Before: GET with enormous query string”
GET /api/reports?start_date=2026-01-01&end_date=2026-06-30&metrics=revenue,churn,mrr,arr,ltv,cac&group_by=month&filters[region]=latam&filters[plan]=pro,enterprise&filters[team_size]=50-200&include=comparison_period&comparison_start=2025-01-01&comparison_end=2025-06-30&format=detailed&currency=BRL HTTP/1.1
Host: api.example.com
QUERY /api/reports HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"period": {
"start": "2026-01-01",
"end": "2026-06-30"
},
"metrics": ["revenue", "churn", "mrr", "arr", "ltv", "cac"],
"group_by": "month",
"filters": {
"region": "latam",
"plan": ["pro", "enterprise"],
"team_size": "50-200"
},
"comparison": {
"start": "2025-01-01",
"end": "2025-06-30"
},
"format": "detailed",
"currency": "BRL"
}

URLs don’t leak into access logs. Complex filters stay structured. No more 414 URI Too Long surprises.


Aspect POST (before) QUERY (after)
Auto-retry on failure ❌ Unsafe — may duplicate side effects ✅ Idempotent — safe to retry
Native HTTP caching ❌ Caches ignore POST ✅ Cacheable by URI + body
Correct semantics ❌ “Create something” used for “read something” ✅ “I’m asking a question”
CORS preflight Depends on Content-Type Yes (QUERY is not safelisted)
Accept-Query discovery ✅ Server advertises accepted formats
POST /search → Client doesn't know if server processed it. Won't retry.
QUERY /search → Client retries automatically. Spec guarantees: no side effects.
POST /search → 2x origin hit. Always.
QUERY /search → 2nd served from cache (if response carries freshness headers).

  • Response format — JSON, XML, whatever. Response is the same.
  • Status codes200, 404, 400, 500 work the same way.
  • Error handling — Same validation logic and error treatment.
  • Authentication — Bearer tokens, API keys, cookies — all unchanged.
  • Content negotiationAccept header works normally.
  • Request body — Same JSON, same schema, same validation.

The only wire-level difference is the first line: QUERY instead of POST or GET.


Want to see running code in Node.js, Go, Python, and curl?

See real implementations