Skip to main content

a6-recipe-graphql-proxy

Overview

APISIX provides built-in GraphQL support through three variables that let you route and apply policies based on GraphQL query content — without parsing GraphQL yourself:

VariableDescriptionExample Value
graphql_nameOperation name from the query"getUser"
graphql_operationOperation type"query", "mutation"
graphql_root_fieldsTop-level fields requested["user", "orders"]

These variables are extracted automatically from POST requests with Content-Type: application/json or application/graphql, and from GET requests with a query parameter.

When to Use

  • Routing different GraphQL operations to different backends
  • Applying rate limits per operation type (queries vs mutations)
  • Restricting which operations specific consumers can execute
  • Converting REST endpoints to GraphQL queries (degraphql)
  • Adding security layers (auth, rate limiting) to a GraphQL API

Approach A: Operation-Based Routing

Route GraphQL queries and mutations to different backends using the graphql_operation variable.

Route queries to read replicas, mutations to primary

# Queries → read replica
a6 route create -f - <<'EOF'
{
"id": "graphql-queries",
"uri": "/graphql",
"vars": [["graphql_operation", "==", "query"]],
"upstream": {
"type": "roundrobin",
"nodes": { "graphql-read-replica:4000": 1 }
}
}
EOF

# Mutations → primary database
a6 route create -f - <<'EOF'
{
"id": "graphql-mutations",
"uri": "/graphql",
"vars": [["graphql_operation", "==", "mutation"]],
"upstream": {
"type": "roundrobin",
"nodes": { "graphql-primary:4000": 1 }
}
}
EOF

Route by operation name

# Route the expensive "analytics" query to a dedicated backend
a6 route create -f - <<'EOF'
{
"id": "graphql-analytics",
"uri": "/graphql",
"vars": [["graphql_name", "==", "getAnalytics"]],
"priority": 10,
"upstream": {
"type": "roundrobin",
"nodes": { "analytics-backend:4000": 1 }
}
}
EOF

The priority field ensures this route is matched before a generic /graphql route.

Approach B: Per-Operation Rate Limiting

Apply different rate limits to queries vs mutations.

# Queries: 1000 req/min
a6 route create -f - <<'EOF'
{
"id": "graphql-query-limited",
"uri": "/graphql",
"vars": [["graphql_operation", "==", "query"]],
"plugins": {
"key-auth": {},
"limit-count": {
"count": 1000,
"time_window": 60,
"key_type": "var",
"key": "consumer_name",
"rejected_code": 429
}
},
"upstream": {
"type": "roundrobin",
"nodes": { "graphql-backend:4000": 1 }
}
}
EOF

# Mutations: 100 req/min (more restrictive)
a6 route create -f - <<'EOF'
{
"id": "graphql-mutation-limited",
"uri": "/graphql",
"vars": [["graphql_operation", "==", "mutation"]],
"plugins": {
"key-auth": {},
"limit-count": {
"count": 100,
"time_window": 60,
"key_type": "var",
"key": "consumer_name",
"rejected_code": 429,
"rejected_msg": "Mutation rate limit exceeded"
}
},
"upstream": {
"type": "roundrobin",
"nodes": { "graphql-backend:4000": 1 }
}
}
EOF

Approach C: Restrict Operations by Consumer

Use consumer-restriction to allow only specific consumers to execute mutations.

a6 route create -f - <<'EOF'
{
"id": "graphql-mutations-restricted",
"uri": "/graphql",
"vars": [["graphql_operation", "==", "mutation"]],
"plugins": {
"key-auth": {},
"consumer-restriction": {
"whitelist": ["admin-user", "service-account"],
"rejected_code": 403,
"rejected_msg": "Mutations not allowed for your account"
}
},
"upstream": {
"type": "roundrobin",
"nodes": { "graphql-backend:4000": 1 }
}
}
EOF

Approach D: REST-to-GraphQL with degraphql

The degraphql plugin converts RESTful endpoints into GraphQL queries, allowing REST clients to consume a GraphQL backend.

1. Enable degraphql on a route

a6 route create -f - <<'EOF'
{
"id": "rest-to-graphql-users",
"uri": "/users/:id",
"methods": ["GET"],
"plugins": {
"degraphql": {
"query": "query getUser($id: ID!) { user(id: $id) { id name email } }",
"variables": ["id"]
}
},
"upstream": {
"type": "roundrobin",
"nodes": { "graphql-backend:4000": 1 }
}
}
EOF

REST clients call GET /users/123 and receive the GraphQL response for user(id: "123").

2. Static query (no variables)

a6 route create -f - <<'EOF'
{
"id": "rest-to-graphql-stats",
"uri": "/stats",
"methods": ["GET"],
"plugins": {
"degraphql": {
"query": "{ systemStats { cpu memory uptime } }"
}
},
"upstream": {
"type": "roundrobin",
"nodes": { "graphql-backend:4000": 1 }
}
}
EOF

Declarative GraphQL Config

# apisix-graphql.yaml
routes:
- id: graphql-queries
uri: "/graphql"
vars: [["graphql_operation", "==", "query"]]
plugins:
key-auth: {}
limit-count:
count: 1000
time_window: 60
key_type: var
key: consumer_name
upstream:
type: roundrobin
nodes:
"graphql-read-replica:4000": 1

- id: graphql-mutations
uri: "/graphql"
vars: [["graphql_operation", "==", "mutation"]]
plugins:
key-auth: {}
limit-count:
count: 100
time_window: 60
key_type: var
key: consumer_name
consumer-restriction:
whitelist: ["admin-user", "service-account"]
upstream:
type: roundrobin
nodes:
"graphql-primary:4000": 1
a6 config diff -f apisix-graphql.yaml
a6 config sync -f apisix-graphql.yaml

Gotchas

  • Body size limit — APISIX parses GraphQL from the request body. Default max body size is 1 MiB (configurable via client_max_body_size in APISIX config). Large queries may be rejected.
  • Single operation only — APISIX extracts variables from the first operation in the request. Batched GraphQL queries (multiple operations) are not supported for routing purposes.
  • No WebSocket subscriptions — GraphQL subscriptions over WebSocket are not supported by the built-in GraphQL parsing. You can still proxy WebSocket connections, but without operation-based routing.
  • POST content types — GraphQL parsing works with application/json (standard) and application/graphql (query in body as text). Other content types are not parsed.
  • GET requests — GraphQL variables are read from the query URL parameter (URL-encoded GraphQL query string).
  • vars matching — the vars field on a route accepts an array of conditions. Each condition is ["variable", "operator", "value"]. Multiple conditions are AND-ed together.
  • degraphql limitations — the plugin sends a POST with application/json to the upstream, regardless of the original request method. The variables field maps URI path parameters to GraphQL variables by name.
  • Priority for overlapping routes — when multiple routes match /graphql with different vars, use the priority field to control matching order. Higher priority = matched first.

Verification

# Test query routing
curl -X POST http://localhost:9080/graphql \
-H "Content-Type: application/json" \
-H "apikey: my-key" \
-d '{"query": "query getUser { user(id: 1) { name } }"}'

# Test mutation routing
curl -X POST http://localhost:9080/graphql \
-H "Content-Type: application/json" \
-H "apikey: my-key" \
-d '{"query": "mutation createUser { createUser(name: \"test\") { id } }"}'

# Test rate limiting (should 429 after exceeding limit)
for i in $(seq 1 1001); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST http://localhost:9080/graphql \
-H "Content-Type: application/json" \
-H "apikey: my-key" \
-d '{"query": "{ users { id } }"}'
done

# Test REST-to-GraphQL
curl http://localhost:9080/users/123
# Returns GraphQL response for user(id: "123")

This page is generated from a6-recipe-graphql-proxy/SKILL.md in the api7/a6 repository. Browse all skills on the AI Agent Skills page.

API7.ai Logo

The digital world is connected by APIs,
API7.ai exists to make APIs more efficient, reliable, and secure.

Sign up for API7 newsletter

Product

API7 Gateway

SOC2 Type IIISO 27001HIPAAGDPRRed Herring

Copyright © APISEVEN PTE. LTD 2019 – 2026. Apache, Apache APISIX, APISIX, and associated open source project names are trademarks of the Apache Software Foundation