Docs
API7 GatewayManage traffic and APIsProtocol ProxyConfigure GraphQL Proxying

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.6 or later. The cache-removal route uses PURGE, which the API7 Gateway Admin API schema accepts starting in 3.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 -l

    Expected: 200, then 1 or 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, and awk are available. The deterministic policy: local check requires GATEWAY_URL to 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; }; done

    ADC resolves --gateway-group by display name; the compatibility Admin API uses the group's UUID. Enter the display name of the group identified by API7_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.

graphql-adc.yaml
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-proxy

Expected: 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-proxy

Expected: 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 } }"}'
done

Expected: 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):/'
done

Expected: 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}"; done

For ADC, save and reconcile the empty selected state, then remove only this guide's files:

graphql-cleanup.yaml
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.yaml

Expected: only the selected resources are removed, and all four routes then return 404.

Troubleshoot

SymptomCauseFix
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.