GraphQL

GraphQL queries, mutations, and the GitHub GraphQL API via gh CLI.

Query Fundamentals

Basic query — request specific fields (no over-fetching)
curl -s -X POST https://api.github.com/graphql \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ viewer { login name company } }"}' | jq .
Query with variables — parameterized, reusable
curl -s -X POST https://api.github.com/graphql \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { stargazerCount description } }",
    "variables": {"owner": "anthropics", "name": "claude-code"}
  }' | jq '.data.repository'

GitHub GraphQL (gh CLI)

gh api graphql — built-in GraphQL support
gh api graphql -f query='
  query {
    viewer {
      login
      repositories(first: 10, orderBy: {field: UPDATED_AT, direction: DESC}) {
        nodes {
          name
          updatedAt
          isPrivate
        }
      }
    }
  }
' | jq '.data.viewer.repositories.nodes[] | {name, updatedAt}'
Query with variables via gh
gh api graphql \
  -f query='query($owner: String!, $name: String!) {
    repository(owner: $owner, name: $name) {
      issues(first: 5, states: OPEN) {
        nodes { number title createdAt }
      }
    }
  }' \
  -f owner="owner" \
  -f name="repo" | jq '.data.repository.issues.nodes[]'

Mutations

Create an issue via GraphQL mutation
gh api graphql -f query='
  mutation($repoId: ID!, $title: String!, $body: String) {
    createIssue(input: {repositoryId: $repoId, title: $title, body: $body}) {
      issue {
        number
        url
      }
    }
  }
' -f repoId="$REPO_NODE_ID" \
  -f title="Bug: API returns 500 on empty payload" \
  -f body="Steps to reproduce..."

Pagination (Relay Cursor)

GraphQL uses cursor-based pagination by convention
gh api graphql -f query='
  query($cursor: String) {
    viewer {
      repositories(first: 50, after: $cursor) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes {
          name
        }
      }
    }
  }
' | jq '{repos: [.data.viewer.repositories.nodes[].name], next: .data.viewer.repositories.pageInfo}'
Loop through all pages
cursor=""
while true; do
    if [[ -z "$cursor" ]]; then
        result=$(gh api graphql -f query='{ viewer { repositories(first:100) { pageInfo { hasNextPage endCursor } nodes { name } } } }')
    else
        result=$(gh api graphql -f query='query($c:String!){ viewer { repositories(first:100, after:$c) { pageInfo { hasNextPage endCursor } nodes { name } } } }' -f c="$cursor")
    fi
    echo "$result" | jq -r '.data.viewer.repositories.nodes[].name'
    has_next=$(echo "$result" | jq -r '.data.viewer.repositories.pageInfo.hasNextPage')
    [[ "$has_next" != "true" ]] && break
    cursor=$(echo "$result" | jq -r '.data.viewer.repositories.pageInfo.endCursor')
done

Introspection

Discover available types and fields
gh api graphql -f query='
  {
    __schema {
      queryType { name }
      types { name kind description }
    }
  }
' | jq '.data.__schema.types[] | select(.kind == "OBJECT") | .name' | head -20
Explore fields on a specific type
gh api graphql -f query='
  {
    __type(name: "Repository") {
      fields { name type { name kind } }
    }
  }
' | jq '.data.__type.fields[] | {name, type: .type.name}'

Python httpx with GraphQL

GraphQL query via httpx
import httpx

query = """
query($owner: String!, $name: String!) {
    repository(owner: $owner, name: $name) {
        stargazerCount
        issues(states: OPEN) { totalCount }
    }
}
"""

response = httpx.post(
    "https://api.github.com/graphql",
    headers={"Authorization": f"Bearer {token}"},
    json={"query": query, "variables": {"owner": "owner", "name": "repo"}},
)
data = response.json()["data"]["repository"]
print(f"Stars: {data['stargazerCount']}, Open Issues: {data['issues']['totalCount']}")

GraphQL vs REST

Aspect          GraphQL                     REST
Over-fetching   Client picks fields         Server decides payload
Under-fetching  Single query, nested data   Multiple endpoints, N+1
Versioning      No versions (evolve schema) /v1/, /v2/ endpoints
Caching         Complex (POST only)         Simple (HTTP cache, ETag)
Tooling         Schema introspection        OpenAPI/Swagger
When to use     Complex relational data     Simple CRUD, caching needed