F frank.ferreira

Caçando Pokémon com Req: consumindo APIs no Elixir

Uma cascata de oito APIs públicas com Req no Elixir: do seu CEP até a cadeia de evolução de um Pokémon, passando por mapa, clima e nascer do sol. Tudo rodando no iex.

Caçando Pokémon com Req

Sábado à noite, eu tava deitado no sofá vendo Pokémon no celular. Episódio 13, aquele em que o Ash chega no farol e aparece uma sombra gigante na névoa. O Dragonite. Dois metros de dragão voador aparecendo pela primeira vez, e o Bill tentando se comunicar com ele. Já vi esse episódio umas dez vezes. Ainda acho bonito.

E aí veio aquela ideia besta que só aparece quando você devia estar dormindo: e se eu montasse uma caçada inteira de Pokémon só com API?

Eu sei, eu sei. Calma. Todo tutorial de API em tech, uma hora, acaba virando uma Pokédex. É quase lei da natureza. Virou o “hello world” de quem vai ensinar a consumir dados de algum lugar, e a essa altura tá tão manjado que dá até preguiça só de anunciar. Mas segura o revirar de olho, porque a gente não vai montar mais uma Pokédex paradona pra listar os 151. A brincadeira aqui é outra: finge que você é aquele moleque que acabou de calçar o tênis novo e saiu de casa pra virar treinador de Pokémon. Cada API é uma parada da jornada. Você sai do seu CEP, descobre onde tá no mapa, vê se vai chover, checa a que horas o sol se põe, encontra um Pokémon no caminho, lê a Pokédex, estuda as fraquezas do bicho, e no fim descobre de onde ele veio. Tudo por código.

Levantei, abri o notebook, abri o iex. O que saiu foi esse artigo.

A gente vai consumir oito APIs públicas de verdade usando o Req, que é o client HTTP mais completo que o Elixir tem hoje. Cada API alimenta a próxima numa cascata: o resultado de uma vira a entrada da outra. Nenhuma pede chave. E tudo roda no terminal, copiável e colável.

Antes de tudo: o que é um client HTTP?

Se você nunca mexeu com API, calma. Deixa eu explicar do começo.

Quando você abre o navegador e digita um endereço, tipo google.com, o navegador manda um pedido pra um servidor em algum lugar do mundo. Esse pedido se chama requisição HTTP. O servidor responde com dados: a página, uma imagem, um monte de texto. Essa resposta se chama, adivinha, resposta HTTP.

Um client HTTP faz exatamente isso, só que por código. Em vez de abrir o Chrome e clicar num link, você escreve uma linha de Elixir. Ela vai lá, bate no servidor, pega a resposta e te devolve os dados. É o seu navegador invisível.

E o JSON? É um formato de texto que as APIs usam pra devolver dados organizados. Parece um mapa, com chaves e valores: {"nome": "Frank", "cidade": "Belém"}. O Elixir sabe ler isso e transformar num mapa de verdade, que você acessa com dados["nome"].

Por que o Req, e não os outros

No Elixir existem vários clients HTTP. Os três mais conhecidos são o HTTPoison, o Tesla e o Req.

O HTTPoison foi por muito tempo o padrão da comunidade. Funciona bem, mas por baixo ele usa uma biblioteca do Erlang chamada hackney, que arrasta umas dependências junto e às vezes complica configuração de segurança (SSL). E o corpo da resposta vem como string crua, então você precisa decodificar o JSON na mão com Jason.decode! toda vez. Eu apanhei com isso no começo, esquecia o decode! e ficava sem entender por que o body["nome"] explodia.

O Tesla é bem flexível. Ele trabalha com middlewares, que são pedaços de código que você empilha pra configurar a requisição antes de ela sair. Dá pra trocar o motor por baixo. Se você vem do Ruby, é parecido com o Faraday. Bom client, mas exige mais configuração pra coisa que o Req já faz sozinho.

O Req é o mais novo. Foi feito pelo Wojtek Mach, que é do core team do Elixir. Ele usa o Finch por baixo, que é rápido e leve. O grande lance é que ele já vem com tudo ligado de fábrica: decodifica JSON sozinho, segue redirect, tenta de novo quando dá erro, comprime o corpo. Sem você configurar nada.

Olha a mesma chamada nos três:

# HTTPoison
{:ok, %HTTPoison.Response{body: body}} = HTTPoison.get("https://api.github.com/repos/wojtekmach/req")
data = Jason.decode!(body)
data["description"]

# Tesla
client = Tesla.client([Tesla.Middleware.JSON])
{:ok, %Tesla.Env{body: body}} = Tesla.get(client, "https://api.github.com/repos/wojtekmach/req")
body["description"]

# Req
Req.get!("https://api.github.com/repos/wojtekmach/req").body["description"]

Uma linha. Fez o GET, decodificou o JSON e te devolveu o mapa. Sem adapter, sem middleware, sem Jason.decode!. Quando você tá encadeando oito APIs numa noite de sábado, isso pesa.

Preparando o terreno

Tudo aqui roda com Mix.install, sem criar projeto nenhum. Abre o iex e cola:

Mix.install([{:req, "~> 0.5.0"}])

Isso baixa o Req e as dependências direto no terminal. Se você já mexeu com Python, é tipo um pip install que funciona dentro do próprio REPL. (REPL é o terminal interativo da linguagem, aquele onde você digita código e vê o resultado na hora. No Elixir ele se chama iex.)

Pronto. Bora pra caçada.

Capítulo 1: De onde eu estou?

API: BrasilAPI (CEP v2) · documentação

BrasilAPI

Toda aventura começa num lugar. Eu moro em Belém do Pará, então vou usar um CEP daqui. A BrasilAPI é uma API pública brasileira que junta vários serviços num lugar só (CEP, CNPJ, bancos, feriados), de graça e sem chave.

cep = "66017000"

resp = Req.get!("https://brasilapi.com.br/api/cep/v2/#{cep}")
endereco = resp.body

IO.puts("""
Endereco encontrado:
  Rua:     #{endereco["street"]}
  Bairro:  #{endereco["neighborhood"]}
  Cidade:  #{endereco["city"]}
  Estado:  #{endereco["state"]}
""")
Endereco encontrado:
  Rua:     Avenida Presidente Vargas
  Bairro:  Campina
  Cidade:  Belém
  Estado:  PA

Repara no resp.body: já é um mapa Elixir. O Req viu que a resposta veio com Content-Type: application/json, decodificou sozinho, e eu saí acessando as chaves direto. Nenhum Jason.decode! à vista.

Um aviso, porque isso me pegou escrevendo esse post: se um dia o CEP te devolver um status: 404 com uma mensagem tipo “Todos os serviços de CEP retornaram erro”, não é o teu código. A BrasilAPI busca o CEP em provedores externos (Correios, ViaCEP e outros) e às vezes todos eles caem ao mesmo tempo pra um CEP específico. Troca por outro CEP e segue.

A Avenida Presidente Vargas é uma das mais movimentadas de Belém. Passa o Ver-o-Peso, passa tudo. Bom lugar pra começar.

Capítulo 2: Onde exatamente?

API: Nominatim (OpenStreetMap) · documentação

OpenStreetMap

Pra checar clima e nascer do sol eu preciso de coordenadas, latitude e longitude. A BrasilAPI v2 às vezes traz, às vezes não, depende do provedor que respondeu. Pra garantir, a gente usa a Nominatim, que é o serviço de geocodificação do OpenStreetMap. Geocodificação é o nome bonito pra “transformar um endereço em coordenadas no mapa”.

query = "#{endereco["street"]}, #{endereco["city"]}, #{endereco["state"]}, Brazil"

geo = Req.get!("https://nominatim.openstreetmap.org/search",
  params: [q: query, format: "json", limit: 1],
  headers: [{"user-agent", "pokemon-hunt-tutorial/1.0"}]
).body |> List.first()

lat = geo["lat"]
lon = geo["lon"]

IO.puts("""
Coordenadas:
  Latitude:  #{lat}
  Longitude: #{lon}
""")
Coordenadas:
  Latitude:  -1.4557549
  Longitude: -48.4901878

Duas coisas pra guardar. O params do Req monta a query string pra você. Passei uma keyword list, [q: query, format: "json", limit: 1], e ele transformou em ?q=Avenida+Presidente+Vargas...&format=json&limit=1. Sem concatenar string, sem URI.encode.

A outra: a Nominatim pede que você mande um header User-Agent identificando sua aplicação. Sem isso ela pode te recusar. O Req aceita headers como uma lista de tuplas, e é isso que aquele {"user-agent", ...} faz.

Coordenada na mão, dá pra ver o ponto no mapa:

Mapa de Belém no ponto da caçada

Mapa © colaboradores do OpenStreetMap.

É o centro de Belém: Campina, Nazaré, a Cidade Velha ali embaixo, e a baía do Guajará na esquerda. O pino tá no ponto que a Nominatim me devolveu.

Capítulo 3: Vai chover?

API: Open-Meteo · documentação

Open-Meteo

Quem mora em Belém sabe: chove quase todo dia. O céu tá limpo e em dez minutos vem um temporal. Antes de sair caçando qualquer coisa, melhor checar.

A Open-Meteo é uma API de previsão do tempo, gratuita e sem chave. Você manda latitude e longitude, ela devolve o tempo agora, a previsão das próximas horas, o que você pedir.

clima = Req.get!("https://api.open-meteo.com/v1/forecast",
  params: [
    latitude: lat,
    longitude: lon,
    current: "temperature_2m,relative_humidity_2m,rain,wind_speed_10m,weather_code",
    timezone: "America/Belem"
  ]
).body["current"]

descricao_tempo = case clima["weather_code"] do
  0 -> "Ceu limpo"
  1 -> "Quase limpo"
  2 -> "Parcialmente nublado"
  3 -> "Nublado"
  code when code in [61, 63, 65] -> "Chovendo"
  code when code in [80, 81, 82] -> "Pancadas de chuva"
  code when code in [95, 96, 99] -> "Tempestade"
  _ -> "Codigo #{clima["weather_code"]}"
end

IO.puts("""
Clima agora em Belem:
  Tempo:       #{descricao_tempo}
  Temperatura: #{clima["temperature_2m"]}C
  Umidade:     #{clima["relative_humidity_2m"]}%
  Vento:       #{clima["wind_speed_10m"]} km/h
  Chuva:       #{clima["rain"]} mm
""")
Clima agora em Belem:
  Tempo:       Parcialmente nublado
  Temperatura: 28.3C
  Umidade:     78%
  Vento:       12.5 km/h
  Chuva:       0.0 mm

O weather_code segue um padrão da Organização Meteorológica Mundial (a WMO). É só um número: 0 é céu limpo, 61 é chuva leve, 95 é tempestade. Fiz um case simples pra traduzir os que interessam. Num projeto de verdade você faria uma função separada com os quase cem códigos, mas pra uma noite no iex isso resolve.

Zero de chuva, 28 graus. Belém colaborando, coisa rara.

Capítulo 4: Que horas o sol se põe?

API: Sunrise-Sunset · documentação

Sunrise-Sunset

Caçar no escuro não rola. A não ser que você queira Pokémon do tipo Ghost, aí é outra conversa. Vamos ver quanta luz ainda sobra.

sol = Req.get!("https://api.sunrise-sunset.org/json",
  params: [lat: lat, lng: lon, date: "today", formatted: 0]
).body["results"]

nascer = sol["sunrise"] |> String.split("T") |> List.last() |> String.split("+") |> List.first()
por = sol["sunset"] |> String.split("T") |> List.last() |> String.split("+") |> List.first()

IO.puts("""
Horarios do sol (UTC):
  Nascer: #{nascer}
  Por:    #{por}
  Duracao do dia: #{sol["day_length"]}s (#{div(sol["day_length"], 3600)}h#{rem(div(sol["day_length"], 60), 60)}min)
""")
Horarios do sol (UTC):
  Nascer: 09:09:42
  Por:    21:21:18
  Duracao do dia: 43896s (12h11min)

Os horários vêm em UTC porque passei formatted: 0, que pede o formato ISO 8601. Belém tá em UTC-3, então na prática o sol se põe por volta das 18:21 no horário local. Tempo de sobra.

Repara na gambiarra: encadeei uns String.split pra arrancar só a hora do timestamp. Não é a coisa mais bonita do mundo, eu sei. Num projeto real eu usaria o DateTime do Elixir pra fazer isso direito. Mas era sábado à noite e a preguiça falou mais alto.

Capítulo 5: O encontro

API: PokéAPI (pokemon) · documentação

PokéAPI

Sol brilhando, céu aberto, coordenada na mão. Hora de encontrar um Pokémon.

A PokéAPI é uma API gratuita com dados de todos os Pokémon que já existiram, mais de mil. Sem autenticação, sem limite agressivo. É uma das melhores APIs públicas que existem pra aprender.

Vou sortear um da primeira geração, de 1 a 151, porque os clássicos são os melhores. Luto por isso.

pokemon_id = Enum.random(1..151)

pokemon = Req.get!("https://pokeapi.co/api/v2/pokemon/#{pokemon_id}").body

tipos = Enum.map(pokemon["types"], fn t -> t["type"]["name"] end)
habilidades = Enum.map(pokemon["abilities"], fn a -> a["ability"]["name"] end)

sprite_url = pokemon["sprites"]["other"]["official-artwork"]["front_default"]

IO.puts("""
Pokemon encontrado!

  ##{pokemon["id"]} #{String.upcase(pokemon["name"])}
  Tipos:       #{Enum.join(tipos, " / ")}
  Altura:      #{pokemon["height"] / 10} m
  Peso:        #{pokemon["weight"] / 10} kg
  Habilidades: #{Enum.join(habilidades, ", ")}

  Artwork: #{sprite_url}
""")
Pokemon encontrado!

  #149 DRAGONITE
  Tipos:       dragon / flying
  Altura:      2.2 m
  Peso:        210.0 kg
  Habilidades: inner-focus, multiscale

  Artwork: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/149.png

Não é que caiu justo ele. Olha:

Dragonite

Dois metros e vinte, 210 quilos, com cara de quem te daria um abraço. Mas é um dragão voador. Respeita.

O campo sprites da PokéAPI é enorme. Tem sprite de frente, de costas, shiny, versão dos jogos antigos, e o official-artwork, que é o mais bonito. Tudo como URL pública, dá pra usar direto num <img> no seu site. A altura vem em decímetros e o peso em hectogramas, porque a PokéAPI segue as unidades dos jogos originais. Dividi por 10 pra virar metro e quilo.

Capítulo 6: O que diz a Pokédex?

API: PokéAPI (pokemon-species) · documentação

Achei o bicho. Mas o que a Pokédex fala dele? A PokéAPI tem um endpoint separado pra dados da espécie, incluindo a descrição em vários idiomas.

especie = Req.get!("https://pokeapi.co/api/v2/pokemon-species/#{pokemon["id"]}").body

entrada = Enum.find(especie["flavor_text_entries"], fn e ->
  e["language"]["name"] == "en" and e["version"]["name"] == "red"
end)

texto = entrada["flavor_text"] |> String.replace("\n", " ") |> String.replace("\f", " ")

genero = especie["genera"] |> Enum.find(& &1["language"]["name"] == "en") |> Map.get("genus")

IO.puts("""
Pokedex (##{pokemon["id"]} - #{genero}):

  "#{texto}"

  Taxa de captura: #{especie["capture_rate"]}/255
  Felicidade base: #{especie["base_happiness"]}
  Cor:             #{especie["color"]["name"]}
  Habitat:         #{especie["habitat"]["name"]}
""")
Pokedex (#149 - Dragon Pokémon):

  "An extremely rarely seen marine POKéMON. Its intelligence is said to match that of humans."

  Taxa de captura: 45/255
  Felicidade base: 35
  Cor:             brown
  Habitat:         rare

Taxa de captura 45 de 255. Baixa. Na prática isso quer dizer que, sem uma Ultra Ball e um Pokémon que coloque o alvo pra dormir, vai ser sofrido. Habitat “rare”, porque né.

Repara na cascata acontecendo: usei o pokemon["id"] que veio da chamada anterior como entrada dessa aqui. Uma API alimenta a outra.

Uma chatice: o flavor_text vem com \n e \f (form feed) no meio, porque é extraído direto dos dados dos jogos antigos. Por isso os dois String.replace. Feio, mas resolve.

Capítulo 7: Conhecendo o inimigo

API: PokéAPI (type) · documentação

Se eu fosse treinador de verdade, precisava saber: o que bate forte nele, e contra o que ele é forte? As relações de dano ficam num endpoint separado. A resposta do Pokémon me deu os tipos (dragon, flying), então agora pego cada tipo e consulto.

IO.puts("Analise de combate:\n")

Enum.each(tipos, fn tipo ->
  relacoes = Req.get!("https://pokeapi.co/api/v2/type/#{tipo}").body["damage_relations"]

  fraco   = Enum.map(relacoes["double_damage_from"], & &1["name"])
  forte   = Enum.map(relacoes["double_damage_to"], & &1["name"])
  resiste = Enum.map(relacoes["half_damage_from"], & &1["name"])
  imune   = Enum.map(relacoes["no_damage_from"], & &1["name"])

  IO.puts("""
    #{String.upcase(tipo)}
    Fraco contra: #{Enum.join(fraco, ", ")}
    Forte contra: #{Enum.join(forte, ", ")}
    Resiste a:    #{Enum.join(resiste, ", ")}
    Imune a:      #{if imune == [], do: "nada", else: Enum.join(imune, ", ")}
  """)
end)
Analise de combate:

  DRAGON
  Fraco contra: ice, dragon, fairy
  Forte contra: dragon
  Resiste a:    fire, water, grass, electric
  Imune a:      nada

  FLYING
  Fraco contra: rock, electric, ice
  Forte contra: fighting, bug, grass
  Resiste a:    fighting, bug, grass
  Imune a:      ground

Gelo aparece nas duas listas de fraqueza. Dano dobrado de gelo pelo tipo Dragon, dano dobrado de gelo pelo tipo Flying. Na prática isso vira dano quádruplo no Dragonite. Leva um Lapras ou um Articuno e acabou.

Mas olha o outro lado: imune a ground por causa do tipo Flying, e resiste a fire, water, grass e electric por causa do Dragon. O bicho não é frágil. Só tem aquele calcanhar de Aquiles congelado.

Capítulo 8: De onde ele veio?

API: PokéAPI (evolution-chain) · documentação

Última etapa. O Dragonite não nasce Dragonite. Ele começa pequeno e vai evoluindo. A PokéAPI guarda a cadeia de evolução num endpoint separado, e o endpoint de espécie (que a gente já chamou) traz a URL dessa cadeia.

url_evolucao = especie["evolution_chain"]["url"]
cadeia = Req.get!(url_evolucao).body["chain"]

nomes = [cadeia["species"]["name"]]

nomes =
  if cadeia["evolves_to"] != [] do
    segundo = hd(cadeia["evolves_to"])
    nomes = nomes ++ [segundo["species"]["name"]]

    if segundo["evolves_to"] != [] do
      nomes ++ [hd(segundo["evolves_to"])["species"]["name"]]
    else
      nomes
    end
  else
    nomes
  end

IO.puts("Cadeia de evolucao:\n")

Enum.each(Enum.with_index(nomes, 1), fn {nome, idx} ->
  evo = Req.get!("https://pokeapi.co/api/v2/pokemon/#{nome}").body
  IO.puts("  #{idx}. #{String.upcase(nome)} (##{evo["id"]})")
end)
Cadeia de evolucao:

  1. DRATINI (#147)
  2. DRAGONAIR (#148)
  3. DRAGONITE (#149)

Dratini, Dragonair e Dragonite

Dratini, uma cobrinha de 1,8 m com cara de inofensiva. Vira Dragonair, uma serpente azul meio mágica. E depois vira Dragonite, um dragão gordo e simpático de 2,2 m. A natureza é estranha.

E olha o que rolou aqui: chamei a PokéAPI de novo dentro de um loop, uma vez por estágio da evolução, pra pegar o número de cada um. Cada chamada é rápida porque o Req usa connection pooling por baixo (via Finch): ele reaproveita a mesma conexão de rede em vez de abrir uma nova toda vez.

A cascata inteira

Oito chamadas, cada uma alimentando a próxima. Junta tudo num script pra copiar e colar:

Mix.install([{:req, "~> 0.5.0"}])

# 1. CEP -> Endereco (BrasilAPI)
cep = "66017000"
endereco = Req.get!("https://brasilapi.com.br/api/cep/v2/#{cep}").body
IO.puts("#{endereco["street"]}, #{endereco["neighborhood"]} - #{endereco["city"]}/#{endereco["state"]}")

# 2. Endereco -> Coordenadas (Nominatim)
query = "#{endereco["street"]}, #{endereco["city"]}, #{endereco["state"]}, Brazil"
geo = Req.get!("https://nominatim.openstreetmap.org/search",
  params: [q: query, format: "json", limit: 1],
  headers: [{"user-agent", "pokemon-hunt/1.0"}]
).body |> List.first()
{lat, lon} = {geo["lat"], geo["lon"]}
IO.puts("Lat: #{lat}, Lon: #{lon}")

# 3. Coordenadas -> Clima (Open-Meteo)
clima = Req.get!("https://api.open-meteo.com/v1/forecast",
  params: [latitude: lat, longitude: lon, current: "temperature_2m,rain,weather_code", timezone: "America/Belem"]
).body["current"]
IO.puts("#{clima["temperature_2m"]}C | Chuva: #{clima["rain"]}mm | Codigo: #{clima["weather_code"]}")

# 4. Coordenadas -> Nascer/Por do sol (Sunrise-Sunset)
sol = Req.get!("https://api.sunrise-sunset.org/json",
  params: [lat: lat, lng: lon, date: "today", formatted: 0]
).body["results"]
IO.puts("Dia com #{div(sol["day_length"], 3600)}h#{rem(div(sol["day_length"], 60), 60)}min de luz")

# 5. Pokemon aleatorio (PokeAPI)
pokemon_id = Enum.random(1..151)
pokemon = Req.get!("https://pokeapi.co/api/v2/pokemon/#{pokemon_id}").body
tipos = Enum.map(pokemon["types"], & &1["type"]["name"])
IO.puts("#{String.upcase(pokemon["name"])} (##{pokemon["id"]}) - #{Enum.join(tipos, "/")}")

# 6. Pokedex (PokeAPI species)
especie = Req.get!("https://pokeapi.co/api/v2/pokemon-species/#{pokemon["id"]}").body
entrada = Enum.find(especie["flavor_text_entries"], & &1["language"]["name"] == "en")
if entrada, do: IO.puts(String.replace(entrada["flavor_text"], ~r/[\n\f]/, " "))

# 7. Fraquezas (PokeAPI type)
tipo = List.first(tipos)
fraquezas = Req.get!("https://pokeapi.co/api/v2/type/#{tipo}").body["damage_relations"]["double_damage_from"]
IO.puts("Fraco contra: #{Enum.map(fraquezas, & &1["name"]) |> Enum.join(", ")}")

# 8. Evolucao (PokeAPI evolution-chain)
cadeia = Req.get!(especie["evolution_chain"]["url"]).body["chain"]
nomes = [cadeia["species"]["name"]]
nomes = if cadeia["evolves_to"] != [], do: nomes ++ [hd(cadeia["evolves_to"])["species"]["name"]], else: nomes
nomes =
  if cadeia["evolves_to"] != [] and hd(cadeia["evolves_to"])["evolves_to"] != [],
    do: nomes ++ [hd(hd(cadeia["evolves_to"])["evolves_to"])["species"]["name"]],
    else: nomes
IO.puts("Evolucao: #{Enum.join(nomes, " -> ")}")

Roda isso e sai algo assim:

Avenida Presidente Vargas, Campina - Belém/PA
Lat: -1.4557549, Lon: -48.4901878
28.3C | Chuva: 0.0mm | Codigo: 2
Dia com 12h11min de luz
DRAGONITE (#149) - dragon/flying
An extremely rarely seen marine POKéMON. Its intelligence is said to match that of humans.
Fraco contra: ice, dragon, fairy
Evolucao: dratini -> dragonair -> dragonite

Oito APIs, uma cascata, zero configuração.

Req.new: quando você vai voltar sempre

Se você gostou da PokéAPI e vai ficar chamando ela toda hora, o Req tem o Req.new pra você não repetir a URL inteira sempre.

pokeapi = Req.new(base_url: "https://pokeapi.co/api/v2")

pikachu = Req.get!(pokeapi, url: "/pokemon/pikachu").body
IO.puts("#{pikachu["name"]} - #{pikachu["weight"] / 10} kg")

bulbasaur = Req.get!(pokeapi, url: "/pokemon/bulbasaur").body
IO.puts("#{bulbasaur["name"]} - #{bulbasaur["weight"] / 10} kg")
pikachu - 6.0 kg
bulbasaur - 6.9 kg

O base_url define a raiz, e cada Req.get! recebe só o caminho depois dela. Se você já usou o Axios no JavaScript, é o mesmo conceito de instância com URL base.

O que mais o Req faz

A gente usou só Req.get! aqui, mas tem bem mais. Coisas que eu uso no dia a dia:

Mandar um POST com JSON: Req.post!("https://httpbin.org/post", json: %{nome: "Frank"}). Ele serializa o mapa e ajusta o Content-Type pra você.

Se a requisição falha por timeout ou por um erro 5xx, o Req tenta de novo sozinho. Dá pra ajustar com retry: :transient, mas o padrão já cobre a maioria dos casos.

Streaming me salvou uma vez. Req.get!(url, into: File.stream!("arquivo.zip")) baixa o arquivo em pedaços, sem comer toda a memória. Usei isso pra puxar uns CSV gigantes do Banco Central e funcionou sem drama.

Tem também autenticação com Bearer (auth: {:bearer, "meu-token"}), cache com ETag (cache: true), e plugins da comunidade como o req_s3 pra falar com S3 e o curl_req pra converter um comando cURL em Req.

Se você vem do Python, o Req é o parente mais próximo do requests. A filosofia é a mesma: funcionar bonito com o mínimo de código.

Pra fechar

A gente saiu do CEP 66017-000 em Belém e terminou na cadeia de evolução do Dragonite. Oito APIs públicas, nenhuma chave, um mapa da minha cidade, uns Pokémon de recompensa, e umas quarenta linhas de Elixir.

Troca o CEP pelo seu. Joga um Enum.random(1..1025) pra pegar Pokémon mais novos. Checa o tempo da tua cidade. Se der um Pokémon de gelo num dia de chuva em Belém, aí combina.

Abre o iex, cola o Mix.install, e vai quebrando as coisas.

← anterior
Elixir do zero, parte 3: concorrência e o jeito BEAM
próximo →
você está em dia