(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisboa

Como consumir uma API GraphQL em Python: passo a passo

João Barros 12 de July de 2026 5 min de leitura

Cada vez mais plataformas — GitHub, Shopify, Contentful — expõem os dados por GraphQL em vez de REST. Consumir uma API GraphQL em Python é mais simples do que parece: é sempre um POST para o mesmo URL, com uma query em texto que descreve exatamente os campos que queremos. Segue o caminho completo, do primeiro pedido até um DataFrame pandas pronto a analisar.

Pré-requisitos

  • Python 3.9 ou superior instalado.
  • As bibliotecas requests e pandas: pip install requests pandas.
  • Um endpoint GraphQL. Nos exemplos usamos o público https://countries.trevorblades.com/graphql, que não pede autenticação.
  • Noções básicas de JSON e de dicionários em Python.

Passo 1: Perceber o que muda em relação a REST

Numa API REST existem vários URLs (um por recurso) e o servidor decide que campos devolve. Numa API GraphQL há um único URL e um único método, o POST. O que muda de pedido para pedido é o corpo: uma query onde o cliente pede só os campos de que precisa. Isso elimina o clássico problema de receber 40 campos quando só interessam três.

Passo 2: Escrever a primeira query

A query é apenas texto. Começa por escrevê-la e testá-la no explorador do fornecedor (quase todos têm um) antes de a levar para o Python:

query {
  countries {
    code
    name
    capital
  }
}

Passo 3: Enviar a query com requests

O corpo do pedido é sempre um JSON com a chave query (e, opcionalmente, variables). A resposta útil vem dentro da chave data.

import requests

URL = "https://countries.trevorblades.com/graphql"

QUERY = """
query {
  countries {
    code
    name
    capital
  }
}
"""

response = requests.post(URL, json={"query": QUERY}, timeout=30)
response.raise_for_status()
data = response.json()

print(data["data"]["countries"][:3])

Repara em dois detalhes que evitam dores de cabeça: json= (e não data=) trata da serialização e do cabeçalho correto, e timeout impede que o script fique pendurado para sempre.

Passo 4: Usar variáveis em vez de concatenar strings

Nunca construas a query com f-strings. Declara variáveis com tipo e envia-as no campo variables — é mais seguro, e o servidor pode reaproveitar o plano da query.

QUERY = """
query CountriesByContinent($code: ID!) {
  continent(code: $code) {
    name
    countries {
      code
      name
    }
  }
}
"""

variables = {"code": "EU"}

response = requests.post(
    URL,
    json={"query": QUERY, "variables": variables},
    timeout=30,
)
countries = response.json()["data"]["continent"]["countries"]
print(len(countries))

Passo 5: Autenticar com um token

APIs privadas pedem um token no cabeçalho Authorization. Guarda-o numa variável de ambiente, nunca no código:

import os
import requests

URL = "https://api.github.com/graphql"
headers = {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"}

QUERY = """
query {
  viewer {
    login
  }
}
"""

response = requests.post(URL, json={"query": QUERY}, headers=headers, timeout=30)
print(response.json()["data"]["viewer"]["login"])

Passo 6: Tratar o erro que vem disfarçado de sucesso

Esta é a armadilha número um: uma API GraphQL devolve HTTP 200 mesmo quando a query falha. O raise_for_status() não deteta nada — o erro está na chave errors do JSON. Centraliza tudo numa função:

def run_query(query, variables=None, headers=None):
    response = requests.post(
        URL,
        json={"query": query, "variables": variables or {}},
        headers=headers,
        timeout=30,
    )
    response.raise_for_status()
    body = response.json()
    if "errors" in body:
        messages = [e["message"] for e in body["errors"]]
        raise RuntimeError("GraphQL: " + "; ".join(messages))
    return body["data"]
Se receberes KeyError: 'data', quase de certeza a resposta trazia errors. Imprime o JSON inteiro antes de te queixares da rede.

Passo 7: Paginar com cursores

GraphQL não usa ?page=1. Pede-se um bloco de N registos e o servidor devolve, dentro de pageInfo, um endCursor e um hasNextPage. O ciclo repete-se até hasNextPage ser falso:

QUERY = """
query Repos($cursor: String) {
  viewer {
    repositories(first: 50, after: $cursor) {
      nodes {
        name
        stargazerCount
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
"""

all_repos = []
cursor = None

while True:
    page = run_query(QUERY, {"cursor": cursor}, headers)["viewer"]["repositories"]
    all_repos.extend(page["nodes"])
    if not page["pageInfo"]["hasNextPage"]:
        break
    cursor = page["pageInfo"]["endCursor"]

print(len(all_repos))

Pedir blocos demasiado grandes (por exemplo 1000) costuma dar erro de custo de query. Entre 50 e 100 é o intervalo seguro na maioria das APIs.

Passo 8: Converter o resultado num DataFrame

Como a resposta é JSON aninhado, o json_normalize achata os níveis e deixa a tabela pronta para pandas:

import pandas as pd

df = pd.json_normalize(all_repos)
df.to_csv("repos.csv", index=False)
print(df.head())

Verificar o resultado

Antes de dares o script por fechado, confirma três coisas:

  • O print do primeiro passo mostra registos reais (e não uma lista vazia).
  • Uma query propositadamente errada (troca name por nome) faz o run_query levantar RuntimeError — sinal de que estás mesmo a ler o campo errors.
  • O número de linhas do DataFrame bate certo com o len(all_repos) e com o total que a plataforma mostra na sua interface.

Conclusão

Com um POST, um dicionário de variáveis, uma verificação de errors e um ciclo de cursores tens tudo o que é preciso para extrair dados de praticamente qualquer API GraphQL. O passo seguinte natural é agendar este script (Azure Functions, Power Automate ou um simples cron) e escrever o resultado num Lakehouse ou numa tabela SQL, em vez de um CSV. E fica a pergunta: das APIs que consomes hoje, quantos campos estás a receber sem nunca usar?