Configure GraphQL Proxying
Proxy GraphQL queries, limit query depth, cache responses, and protect manual cache removal.
Applies to:API7 Gateway 3.10.x
Roles:Platform engineer
Goal
Three independent GraphQL endpoints that use one HTTPS upstream: a basic proxy at /graphql/basic, a depth-limited proxy at /graphql/limited, and a cached proxy at /graphql/cached. A fourth, authenticated route exposes only the cache plugin's manual removal API.
The public paths differ so that all three routes can stay active without competing for the same POST /graphql request. The proxy-rewrite (opens in Plugin Hub docs) plugin changes each upstream request path back to /graphql.
Before you begin
Set the variables this guide uses:
export API7_ADMIN_URL="https://localhost:7443"
export API7_GATEWAY_GROUP_ID="default" # only the group the quickstart creates; otherwise copy the gateway group's UUID from Gateway Groups in the Dashboard
read -rp "Gateway group name for ADC (if used): " API7_GATEWAY_GROUP_NAME && export API7_GATEWAY_GROUP_NAME
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Dashboard token: " API7_ADMIN_KEY && export API7_ADMIN_KEY && echo-
Use API7 Gateway
3.10.6or later. The cache-removal route usesPURGE, which the API7 Gateway Admin API schema accepts starting in3.10.6. -
Confirm that the control plane and selected gateway group are ready:
curl -ks -o /dev/null -w '%{http_code}\n' "${API7_ADMIN_URL}/api/gateway_groups" -H "X-API-KEY: ${API7_ADMIN_KEY}" curl -ks "${API7_ADMIN_URL}/api/gateway_groups/${API7_GATEWAY_GROUP_ID}/instances" -H "X-API-KEY: ${API7_ADMIN_KEY}" | grep -o '"status": *"Healthy"' | wc -lExpected:
200, then1or more. -
The gateway can resolve and reach
countries.trevorblades.com:443. This is a public demonstration service, so its availability and data are outside API7 Gateway's control. -
curl,jq, andawkare available. The deterministicpolicy: localcheck requiresGATEWAY_URLto address one gateway instance because different instances keep separate counters. -
For ADC, confirm that it supports label selectors and that this guide's working files do not exist:
adc --version adc sync --help | grep -- '--label-selector' for file in graphql-adc.yaml graphql-cleanup.yaml diff.yaml; do test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }; doneADC resolves
--gateway-groupby display name; the compatibility Admin API uses the group's UUID. Enter the display name of the group identified byAPI7_GATEWAY_GROUP_ID.
This guide creates two services, four routes, one consumer, and one credential. Check every fixed identifier:
for id in graphql-service graphql-cache-purge-service; do curl -ks -w "\nservice ${id}: %{http_code}\n" "${API7_ADMIN_URL}/apisix/admin/services/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"; done
for id in graphql-basic-route graphql-limit-route graphql-cache-route graphql-cache-purge-route; do curl -ks -w "\nroute ${id}: %{http_code}\n" "${API7_ADMIN_URL}/apisix/admin/routes/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"; done
curl -ks -w '\nconsumer: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/consumers/graphql-cache-operator?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -w '\ncredential: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/consumers/graphql-cache-operator/credentials/graphql-cache-operator-cred?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"Expected: every resource is absent. A 200 response containing a matching resource means the ID is in use; stop or change every occurrence. Correct authentication, group, or request errors until absence is unambiguous because the following PUT operations replace matching IDs. For ADC, continue only if its diff shows creates for exactly these resources, without updates, replacements, or deletions.
Steps
Complete steps 1–3 for the Admin API path, or skip them and complete step 4 for ADC. Do not apply both paths.
Step 1: Create the GraphQL service
curl -ks "${API7_ADMIN_URL}/apisix/admin/services/graphql-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{
"name": "graphql-service",
"labels": {"guide": "graphql-proxy"},
"upstream": {
"name": "default", "type": "roundrobin", "scheme": "https", "pass_host": "node",
"nodes": [{"host": "countries.trevorblades.com", "port": 443, "weight": 100}]
}
}'{ "value": { "id": "graphql-service", "name": "graphql-service", … } }What changed: the service points to the demonstration GraphQL upstream; no public route uses it yet.
Step 2: Create the basic, limited, and cached routes
curl -ks "${API7_ADMIN_URL}/apisix/admin/routes/graphql-basic-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{"name":"graphql-basic-route","labels":{"guide":"graphql-proxy"},"paths":["/graphql/basic"],"methods":["POST"],"service_id":"graphql-service","plugins":{"proxy-rewrite":{"uri":"/graphql"}}}'
curl -ks "${API7_ADMIN_URL}/apisix/admin/routes/graphql-limit-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{
"name":"graphql-limit-route","labels":{"guide":"graphql-proxy"},"paths":["/graphql/limited"],"methods":["POST"],"service_id":"graphql-service",
"plugins":{"proxy-rewrite":{"uri":"/graphql"},"graphql-limit-count":{"count":2,"time_window":30,"rejected_code":429,"policy":"local","key_type":"var","key":"remote_addr"}}
}'
curl -ks "${API7_ADMIN_URL}/apisix/admin/routes/graphql-cache-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{
"name":"graphql-cache-route","labels":{"guide":"graphql-proxy"},"paths":["/graphql/cached"],"methods":["POST"],"service_id":"graphql-service",
"plugins":{"proxy-rewrite":{"uri":"/graphql"},"graphql-proxy-cache":{}}
}'{ "value": { "id": "graphql-basic-route", … } }
{ "value": { "id": "graphql-limit-route", … } }
{ "value": { "id": "graphql-cache-route", … } }What changed: all three examples can remain active. The limited route charges query depth to a per-instance, per-client-address counter. The cached route caches queries and bypasses mutations.
Step 3: Protect the manual cache-removal endpoint
Create a service without an upstream, a consumer credential, and a route that exposes the plugin API through public-api (opens in Plugin Hub docs) and restricts it with key-auth (opens in Plugin Hub docs):
curl -ks "${API7_ADMIN_URL}/apisix/admin/services/graphql-cache-purge-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{"name":"graphql-cache-purge-service","labels":{"guide":"graphql-proxy"}}'
curl -ks "${API7_ADMIN_URL}/apisix/admin/routes/graphql-cache-purge-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{"name":"graphql-cache-purge-route","labels":{"guide":"graphql-proxy"},"paths":["/apisix/plugin/graphql-proxy-cache/*"],"methods":["PURGE"],"service_id":"graphql-cache-purge-service","plugins":{"key-auth":{},"public-api":{}}}'
curl -ks "${API7_ADMIN_URL}/apisix/admin/consumers/graphql-cache-operator?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{"username":"graphql-cache-operator","labels":{"guide":"graphql-proxy"}}'
curl -ks "${API7_ADMIN_URL}/apisix/admin/consumers/graphql-cache-operator/credentials/graphql-cache-operator-cred?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{"name":"graphql-cache-operator-cred","plugins":{"key-auth":{"key":"graphql-purge-key"}}}'What changed: the removal handler is exposed only for authenticated PURGE requests. Replace this tutorial key and control who can retrieve it before adapting the configuration for another environment.
Step 4: Apply the complete desired state with ADC
Skip this alternative if you completed steps 1–3. Save every resource in one file. Separate sync files would each describe a different desired state and could remove resources omitted from the next sync.
services:
- name: graphql-service
labels: {guide: graphql-proxy}
upstream:
name: default
scheme: https
pass_host: node
nodes:
- {host: countries.trevorblades.com, port: 443, weight: 100}
routes:
- name: graphql-basic-route
labels: {guide: graphql-proxy}
uris: [/graphql/basic]
methods: [POST]
plugins:
proxy-rewrite: {uri: /graphql}
- name: graphql-limit-route
labels: {guide: graphql-proxy}
uris: [/graphql/limited]
methods: [POST]
plugins:
proxy-rewrite: {uri: /graphql}
graphql-limit-count: {count: 2, time_window: 30, rejected_code: 429, policy: local, key_type: var, key: remote_addr}
- name: graphql-cache-route
labels: {guide: graphql-proxy}
uris: [/graphql/cached]
methods: [POST]
plugins:
proxy-rewrite: {uri: /graphql}
graphql-proxy-cache: {}
- name: graphql-cache-purge-service
labels: {guide: graphql-proxy}
routes:
- name: graphql-cache-purge-route
labels: {guide: graphql-proxy}
uris: [/apisix/plugin/graphql-proxy-cache/*]
methods: [PURGE]
plugins:
key-auth: {}
public-api: {}
consumers:
- username: graphql-cache-operator
labels: {guide: graphql-proxy}
credentials:
- name: graphql-cache-operator-cred
type: key-auth
config: {key: graphql-purge-key}Lint, validate, and inspect the diff:
adc lint -f graphql-adc.yaml
adc validate -f graphql-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=graphql-proxy
adc diff -f graphql-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=graphql-proxyExpected: lint and validation succeed, and the diff creates only the two labeled services with four nested routes and the labeled consumer with one nested credential. Apply that reviewed diff:
adc sync -f graphql-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=graphql-proxyExpected: the sync affects only the resources shown in the reviewed diff.
What changed: ADC reconciled one complete desired state containing every example in this guide.
Validate
Send a basic query:
curl -fsS --connect-timeout 3 --max-time 15 "${GATEWAY_URL}/graphql/basic" -X POST -H "Content-Type: application/json" \
-d '{"query":"query { country(code: \"AD\") { code name } }"}' | jq -r '.data.country | [.code, .name] | @tsv'Expected: AD and Andorra separated by a tab.
Immediately test the depth quota. This depth-2 query consumes the quota of 2, so the second request from the same address to the same instance within 30 seconds is rejected:
for attempt in 1 2; do
curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w "attempt ${attempt}: %{http_code}\n" "${GATEWAY_URL}/graphql/limited" -X POST \
-H "Content-Type: application/json" -d '{"query":"query { continents { code } }"}'
doneExpected: attempt 1: 200, then attempt 2: 429. If this key already has requests in the current window, wait 30 seconds first.
Send the same cacheable query twice:
CACHE_ALIAS="guideCache$(date +%s)"
CACHE_BODY=$(jq -nc --arg alias "${CACHE_ALIAS}" '{query: ("query { " + $alias + ": country(code: \"US\") { name capital } }")}')
for attempt in 1 2; do
curl -sS --connect-timeout 3 --max-time 15 -D - -o /dev/null "${GATEWAY_URL}/graphql/cached" -X POST \
-H "Content-Type: application/json" -d "${CACHE_BODY}" \
| awk 'tolower($0) ~ /^http\// || tolower($0) ~ /^apisix-cache-(key|status):/'
doneExpected: both return 200; the first reports MISS, the second HIT, and both show the same APISIX-Cache-Key.
Resolve the cache route's actual backend ID, capture the key, remove that disk entry, and repeat the query. ADC treats a resource's display name and backend ID as separate fields, so do not substitute the route name into the removal URL:
GRAPHQL_SERVICE_ID=$(curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/services?gateway_group_id=${API7_GATEWAY_GROUP_ID}&search=graphql-service&page=1&page_size=100" \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
| jq -r '[.list[] | select(.name == "graphql-service" and .labels.guide == "graphql-proxy") | .id] | if length == 1 then .[0] else empty end')
test -n "${GRAPHQL_SERVICE_ID}" || { echo 'Expected exactly one labeled graphql-service'; exit 1; }
GRAPHQL_CACHE_ROUTE_ID=$(curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/routes?gateway_group_id=${API7_GATEWAY_GROUP_ID}&service_id=${GRAPHQL_SERVICE_ID}&search=graphql-cache-route&page=1&page_size=100" \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
| jq -r '[.list[] | select(.name == "graphql-cache-route" and .labels.guide == "graphql-proxy") | .id] | if length == 1 then .[0] else empty end')
test -n "${GRAPHQL_CACHE_ROUTE_ID}" || { echo 'Expected exactly one labeled graphql-cache-route'; exit 1; }
CACHE_KEY=$(curl -sS --connect-timeout 3 --max-time 15 -D - -o /dev/null "${GATEWAY_URL}/graphql/cached" -X POST \
-H "Content-Type: application/json" -d "${CACHE_BODY}" \
| awk 'tolower($0) ~ /^apisix-cache-key:/ {gsub("\r", "", $2); print $2}')
test -n "${CACHE_KEY}" || { echo 'No APISIX-Cache-Key returned'; exit 1; }
curl -isS --connect-timeout 3 --max-time 15 "${GATEWAY_URL}/apisix/plugin/graphql-proxy-cache/disk/${GRAPHQL_CACHE_ROUTE_ID}/${CACHE_KEY}" -X PURGE -H "apikey: graphql-purge-key" | head -1
curl -sS --connect-timeout 3 --max-time 15 -D - -o /dev/null "${GATEWAY_URL}/graphql/cached" -X POST \
-H "Content-Type: application/json" -d "${CACHE_BODY}" \
| awk 'tolower($0) ~ /^http\// || tolower($0) ~ /^apisix-cache-(key|status):/'Expected: both management queries resolve exactly one labeled object, PURGE returns 200, and the next identical query reports MISS. The URL names the default disk strategy, actual cache-owning backend route ID, and exact response key.
Roll back / Clean up
For the Admin API path, delete resources in reverse dependency order:
curl -ks -X DELETE "${API7_ADMIN_URL}/apisix/admin/consumers/graphql-cache-operator/credentials/graphql-cache-operator-cred?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -X DELETE "${API7_ADMIN_URL}/apisix/admin/consumers/graphql-cache-operator?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
for id in graphql-cache-purge-route graphql-cache-route graphql-limit-route graphql-basic-route; do curl -ks -X DELETE "${API7_ADMIN_URL}/apisix/admin/routes/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"; done
for id in graphql-cache-purge-service graphql-service; do curl -ks -X DELETE "${API7_ADMIN_URL}/apisix/admin/services/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"; doneFor ADC, save and reconcile the empty selected state, then remove only this guide's files:
services: []
consumers: []adc diff -f graphql-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=graphql-proxy
adc sync -f graphql-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=graphql-proxy
rm graphql-adc.yaml graphql-cleanup.yaml diff.yamlExpected: only the selected resources are removed, and all four routes then return 404.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
A public endpoint returns 404. | Its route is absent, belongs to another group, or the path is wrong. | Check the group and use /graphql/basic, /graphql/limited, or /graphql/cached exactly. |
| The upstream reports a GraphQL-path error. | proxy-rewrite is absent or has the wrong URI. | Set uri: /graphql on that route. |
Both limit requests return 200. | A load balancer sent them to instances with separate local counters. | Address one instance for this check, or use a Redis-backed policy for shared quotas. See graphql-limit-count (opens in Plugin Hub docs). |
A GraphQL plugin returns 413. | The request exceeds graphql.max_size, whose default is 1 MiB. | Reduce it or follow Change GraphQL Request Size. |
The second cache query is not a HIT. | The request, route, service, identity, cacheability, or TTL differs. | Repeat the exact body on the cached route and inspect both cache headers. See graphql-proxy-cache (opens in Plugin Hub docs). |
Cache removal returns 401. | The operator key is missing or wrong. | Send the configured apikey value and verify the credential reached this group. |
Cache removal returns 400 or 404. | A path segment is wrong, a display name was used instead of the actual backend route ID, or the entry no longer exists. | Resolve GRAPHQL_CACHE_ROUTE_ID through the management API and capture CACHE_KEY immediately before removal. |
Related
graphql-limit-countplugin reference (opens in Plugin Hub docs) — query cost and shared-counter policies.graphql-proxy-cacheplugin reference (opens in Plugin Hub docs) — cache keys, strategies, TTLs, and mutation bypass.proxy-rewriteplugin reference (opens in Plugin Hub docs) — upstream path rewriting.public-apiplugin reference (opens in Plugin Hub docs) — exposing plugin-owned APIs through a route.- ADC reference — desired-state validation, diff, and synchronization.