GraphQL over HTTP

Single-endpoint POST, query/variables/operationName, using GET for queries, application/graphql-response+json, the '200 with errors' convention, caching challenges, and batching.

GraphQL is a query language in which the client specifies exactly which fields it needs. Unlike REST, which gives each resource its own URL, GraphQL usually serves all requests through a single endpoint (/graphql by convention). GraphQL itself doesn't mandate a transport, but it runs over HTTP in practice, and that usage is being codified by the GraphQL over HTTP specification.

Single endpoint and POST

The most common approach is to POST to one URL. The request body is JSON with three standard keys: query (the document to execute), variables (the bundle of variable values), and operationName (which operation to run when the document contains several).

POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/graphql-response+json

{"query":"query GetUser($id: ID!){ user(id:$id){ name email } }",
 "variables":{"id":"u_92311"},
 "operationName":"GetUser"}

Using GET for queries

Read-only queries can also be sent with GET. Put query, variables (as URL-encoded JSON), and operationName in the query string. Using GET expresses the request as a URL, so HTTP caches and CDNs can cache the response, and it can be shared or bookmarked from the address bar. State-changing mutations have side effects and must use POST, and large queries are hard to fit into a GET because of URL length limits.

GET /graphql?query=query%7Buser(id%3A%22u_92311%22)%7Bname%7D%7D HTTP/1.1
Host: api.example.com
Accept: application/graphql-response+json

Content type and status-code conventions

The GraphQL over HTTP spec introduces the application/graphql-response+json media type for responses. The older habit of application/json is still widespread, but the new type makes clear that 'this body is a spec-conformant GraphQL response,' letting clients handle errors more precisely. Requests negotiate the type they want via the Accept header.

The most confusing point here is status codes. Traditionally a GraphQL server returned HTTP 200 OK even when a field errored during execution, carrying the actual errors in the body's errors array — because at the HTTP layer the request and response were exchanged fine.

{
  "data": { "user": null },
  "errors": [
    { "message": "User not found",
      "path": ["user"],
      "extensions": { "code": "NOT_FOUND" } }
  ]
}

Caching challenges and batching

One of GraphQL's biggest weaknesses is HTTP caching. Every request is a POST to the same URL with the query in the body, so URL-keyed CDN and browser caches can't distinguish responses. Teams work around this by (1) sending queries via GET to use the URL as a cache key, (2) using sophisticated server/client-side (normalized) caches, or (3) persisted queries — pre-registering a query by hash and sending only a short identifier — to make the URL cache-friendly. Meanwhile, batching multiple operations into one request reduces round trips, but complicates per-response caching, error isolation, and timeouts, so weigh the trade-offs before adopting it.