Exposing GraphQL Through a REST Gateway
Putting a GraphQL service behind an API gateway breaks most of what the gateway does well. Every request is a POST to one path, so path-based routing and per-endpoint quotas collapse into a single bucket; the operation is in the body, so nothing can be decided without parsing it; and one request can cost anything from a microsecond to a minute. This page covers what to do at the gateway, what to push into a GraphQL-aware layer, and how to expose a REST surface over a GraphQL backend when the callers need one.
Prerequisite concepts
This builds on protocol translation patterns for the general transcoding model and on rate limiting and throttling strategies, because the quota problem is where GraphQL and gateways disagree most.
Why the usual controls stop working
What belongs at the gateway anyway
Four controls still work unchanged, and they are worth applying before anything GraphQL-aware.
# Envoy 1.32+ — the controls that still apply to an opaque POST
route:
match: { path: "/graphql", headers: [{ name: ":method", string_match: { exact: "POST" } }] }
route:
cluster: graphql_upstream
timeout: 10s # bound the worst case, whatever it is
typed_per_filter_config:
envoy.filters.http.buffer:
"@type": type.googleapis.com/envoy.extensions.filters.http.buffer.v3.BufferPerRoute
buffer:
max_request_bytes: 65536 # a 4 MB query document is not a legitimate query
Authentication, a request-size cap, a timeout and a connection-level rate limit are all meaningful without understanding the query. Everything beyond that — cost limits, depth limits, per-operation quotas, persisted queries — requires parsing, and belongs in a GraphQL gateway or in the service itself rather than in a generic proxy.
The one control that changes the picture entirely is persisted queries: clients send an identifier instead of a query document, the server holds the allowed set, and suddenly the gateway can route, cache and meter on that identifier like an ordinary endpoint. If you control the clients, this converts the problem back into one your existing infrastructure already solves.
Exposing REST over GraphQL
The reverse direction — REST-shaped endpoints backed by a GraphQL service — is a facade the gateway can build, and it is how most teams serve partners who do not want to learn a schema.
Decision matrix
| Control | Where it belongs |
|---|---|
| Authentication and token validation | gateway |
| Request size cap and timeout | gateway |
| Connection and request-rate limits | gateway |
| Query depth and complexity limits | GraphQL layer |
| Per-operation quotas | gateway, if persisted queries are used |
| Response caching | GraphQL layer, or gateway with persisted queries |
| Field-level authorisation | the service |
| REST facade for partners | gateway |
Gotchas and failure signals
A single request rate limit on /graphql is not a quota, because request cost varies by orders of magnitude. Either adopt persisted queries and meter by operation, or accept that the limit only protects against volume, not against expense.
Errors come back as 200. GraphQL reports failures in the response body, so every gateway metric based on status codes reports perfect health during an outage. Either parse the body for an errors array or rely on upstream metrics rather than gateway ones.
Caching a GraphQL POST is unsafe by default — the body is the cache key and few caches handle that. Persisted queries or a GET-with-query-id convention are the practical ways to make caching possible at all.
Introspection enabled in production hands an attacker a complete map of the schema. Disable it, or restrict it to authenticated internal callers.
The limit that actually protects the backend is a cost budget assigned per caller and decremented by the estimated complexity of each query before it runs. Estimating cost statically from the document — counting fields, multiplying by list sizes declared in arguments — is imperfect but catches the pathological cases, and it fails in the safe direction because an over-estimate rejects a query rather than admitting one that will hurt.
Validation
- Request size capped and an overall timeout set on the GraphQL route
- Cost or depth limiting enforced somewhere, and it is documented where
- Gateway alerting does not rely solely on status codes for this route
- Schema introspection disabled or restricted in production
- Persisted queries used wherever the clients are ones you control
- Any REST facade has its own version and deprecation policy
- A slow-query log exists on the GraphQL service and is reviewed, because the gateway cannot tell you which operations are expensive and the service is the only component in the path that can
- Someone owns the persisted-query set, including how a new operation is added to it and how an unused one is retired from it
FAQ
Why does rate limiting a GraphQL endpoint not work?
Because every request is a POST to the same path, so a request-count limit treats a three-field query and a three-hundred-thousand-field query identically. The limit protects against volume and not against expense. Either adopt persisted queries so the gateway can meter per operation, or move cost-based limiting into a GraphQL-aware layer that can parse the document.
What can the gateway still enforce?
Authentication and token validation, a request body size cap, an overall timeout, and connection or request-rate limits. All four are meaningful without understanding the query. Anything that depends on what the query asks for — depth, complexity, per-operation quotas, field authorisation — requires parsing and belongs elsewhere.
How do persisted queries change things?
They convert the problem back into one the gateway already solves. Clients send an operation identifier instead of a document, the server holds the allowed set, and the gateway can then route, cache and meter on that identifier exactly as it would on a path. If you control the clients, this is the single highest-value change available.
Why do my error dashboards look healthy during a GraphQL outage?
GraphQL returns errors inside a 200 response body, so every gateway metric keyed on status code reports success. Either inspect the body for an errors array before recording the outcome, or base alerting on upstream service metrics rather than on gateway status codes for that route.
Parent: Protocol Translation Patterns
Related
- Protocol Translation Patterns — the general transcoding model this is a special case of.
- Rate Limiting & Throttling Strategies — why a request count is the wrong unit when cost varies.
- Caching & Response Optimization — what makes a POST cacheable, and what does not.