An Interest In:
Web News this Week
- April 26, 2024
- April 25, 2024
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
Elixir: Consumindo dados de uma API externa
Neste post vamos aprender a consumir dados de uma API externa. No momento da criao do usurio, vamos realizar uma chamada na API do ViaCep para validar o CEP dele, buscando e gravando as informaes de cidade e UF desse usurio. Vamos aprender tambm como evitar fazer chamadas desnecessrias.
- Criando o projeto
- Tabela users
- Funo para criar um usurio
- Consumindo dados de uma API externa
- Instalando o Tesla
- Instalando o HTTPoison
- Behaviour para a funo get_cep_info()
- HTTP Client com Tesla
- HTTP Client com HTTPoison
- Validando o CEP na criao do usurio
- Evitando chamadas desnecessarias para API Externa (apply_action)
- User-Agent header
- Concluso
Criando o projeto
$ mix phx.new learning_external_api --app my_app
Link do projeto pronto: https://github.com/maiquitome/learning_external_api
Tabela users
Criando a migration e o schema:
$ mix phx.gen.schema User users first_name last_name email cep city uf
Alterando o schema do user:
Remova city
e uf
apenas do validate_required
.
Criando o banco de dados e rodando as migrations:
$ mix ecto.setup
Funo para criar um usurio
Precisaremos de uma funo para criar um usurio, pois vamos usar a API do ViaCEP para validar o CEP dele. Vamos adicionar essa validao posteriormente.
Em lib/my_app/users/create.ex
:
defmodule MyApp.Users.Create do alias MyApp.{Repo, User} @type user_params :: %{ first_name: String.t(), last_name: integer, cep: String.t(), email: String.t() } @doc """ Inserts a user into the database. ## Examples iex> alias MyApp.{User, Users} ...> ...> user_params = %{ ...> first_name: "Mike", ...> last_name: "Wazowski", ...> cep: "95270000", ...> email: "mike_wazowski@monstros_sa.com" ...> } ...> ...> {:ok, %User{}} = Users.Create.call user_params ...> iex> {:error, %Ecto.Changeset{}} = Users.Create.call %{} """ @spec call(user_params()) :: {:error, Ecto.Changeset.t()} | {:ok, Ecto.Schema.t()} def call(params) do %User{} |> User.changeset(params) |> Repo.insert() endend
Consumindo dados de uma API externa
Para conseguir consumir dados de uma API externa precisamos de um HTTP client. Podemos usar o Tesla ou o HTTPoison. A vantagem do Tesla que ele possui middlewares prontos para usarmos. Neste post vamos usar os dois clients para compar-los.
Instalando o Tesla
Para instalar o Tesla, adicione ao mix.exs
:
defp deps do [ {:tesla, "~> 1.4"}, # opcional, mas recomendado {:hackney, "~> 1.17"} # esse no precisa pois j vem com o phoenix {:jason, ">= 1.0.0"} ]end
Voc pode encontrar a verso mais recente do Tesla em: https://hex.pm/packages/tesla
Instalando as dependncias:
$ mix deps.get
Testando o Tesla:
$ iex -S mix
iex> Tesla.get "https://viacep.com.br/ws/01001000/json/"{:ok, %Tesla.Env{}}iex> Tesla.get "" {:error, {:no_scheme}}iex> Tesla.get "https://exemplo.com" {:error, :econnrefused}
Instalando o HTTPoison
Para instalar o HTTPoison, adicione ao mix.exs
:
defp deps do [ {:httpoison, "~> 1.8"} ]end
Voc pode encontrar a verso mais recente do HTTPoison em: https://hex.pm/packages/httpoison
Instalando as dependncias:
$ mix deps.get
Testando o HTTPoison:
$ iex -S mix
iex> HTTPoison.get "https://viacep.com.br/ws/01001000/json/"{:ok, %HTTPoison.Response{}}iex> HTTPoison.get "" ** (CaseClauseError) no case clause matching: []iex> HTTPoison.get "https://exemplo.com" {:error, %HTTPoison.Error{id: nil, reason: :closed}}
Behaviour para a funo get_cep_info()
Vamos definir um contrato para a funo get_cep_info()
. Quando essa funo for implementada ela obrigatoriamente deve receber uma String
como parmetro e retornar sempre uma tupla de {:ok, map()}
ou {:error, map()}
.
Em lib/my_app/via_cep/behaviour.ex
:
defmodule MyApp.ViaCep.Behaviour do @callback get_cep_info(String.t()) :: {:ok, map()} | {:error, map()}end
Se tentarmos retornar algo diferente para esta funo, receberemos um aviso:
HTTP Client com Tesla
Vamos colocar o nome do arquivo de tesla_client.ex
, pois faremos outro chamado httpoison_client.ex
e, desta forma, conseguiremos comparar os HTTP clients
de um jeito melhor.
Em lib/my_app/via_cep/tesla_client.ex
:
defmodule MyApp.ViaCep.TeslaClient do # Ao invs de Tesla.get(), vc vai usar apenas get() use Tesla alias Tesla.Env @behaviour MyApp.ViaCep.Behaviour @base_url "https://viacep.com.br/ws/" # codifica (encode) os parametros para json # e descodifica (decode) a resposta para json automaticamente. plug Tesla.Middleware.JSON def get_cep_info(url \\ @base_url, cep) do "#{url}#{cep}/json/" |> get() |> handle_get() end # casos abaixo de sucesso e erro defp handle_get({:ok, %Env{status: 200, body: %{"erro" => "true"}}}) do {:error, %{status: :not_found, result: "CEP not found!"}} end defp handle_get({:ok, %Env{status: 200, body: body}}) do {:ok, body} end defp handle_get({:ok, %Env{status: 400, body: _body}}) do {:error, %{status: :bad_request, result: "Invalid CEP!"}} end defp handle_get({:error, reason}) do {:error, %{status: :bad_request, result: reason}} endend
Vamos fazer o teste:
iex(1)> MyApp.ViaCep.TeslaClient.get_cep_info "95270000"{:ok, %{ "bairro" => "", "cep" => "95270-000", "complemento" => "", "ddd" => "54", "gia" => "", "ibge" => "4308201", "localidade" => "Flores da Cunha", "logradouro" => "", "siafi" => "8661", "uf" => "RS" }}iex(2)> MyApp.ViaCep.TeslaClient.get_cep_info "95270001"{:error, %{result: "CEP not found!", status: :not_found}}iex(3)> MyApp.ViaCep.TeslaClient.get_cep_info "" {:error, %{result: "Invalid CEP!", status: :bad_request}}
HTTP Client com HTTPoison
No HTTPoison no temos um middleware pronto para transformar o json
em map
, ento vamos precisar usar o Jason.decode()
que j vem instalado no Phoenix.
Em lib/my_app/via_cep/httpoison_client.ex
:
defmodule MyApp.ViaCep.HttpoisonClient do alias HTTPoison.{Error, Response} @behaviour MyApp.ViaCep.Behaviour @base_url "https://viacep.com.br/ws/" def get_cep_info(url \\ @base_url, cep) do "#{url}#{cep}/json/" |> HTTPoison.get() |> json_to_map() |> handle_get() end defp json_to_map({:ok, %Response{body: body} = response}) do {_ok_or_error, body} = Jason.decode(body) {:ok, Map.put(response, :body, body)} end defp json_to_map({:error, %Error{}} = error), do: error defp handle_get({:ok, %Response{status_code: 200, body: %{"erro" => "true"}}}) do {:error, %{status: :not_found, result: "CEP not found!"}} end defp handle_get({:ok, %Response{status_code: 200, body: body}}) do {:ok, body} end defp handle_get({:ok, %Response{status_code: 400, body: _body}}) do {:error, %{status: :bad_request, result: "Invalid CEP!"}} end defp handle_get({:error, reason}) do {:error, %{status: :bad_request, result: reason}} endend
Vamos fazer o teste:
iex(1)> MyApp.ViaCep.HttpoisonClient.get_cep_info "95270000"{:ok, %{ "bairro" => "", "cep" => "95270-000", "complemento" => "", "ddd" => "54", "gia" => "", "ibge" => "4308201", "localidade" => "Flores da Cunha", "logradouro" => "", "siafi" => "8661", "uf" => "RS" }}iex(2)> MyApp.ViaCep.HttpoisonClient.get_cep_info "95270001"{:error, %{result: "CEP not found!", status: :not_found}}iex(3)> MyApp.ViaCep.HttpoisonClient.get_cep_info "" {:error, %{result: "Invalid CEP!", status: :bad_request}}
Validando o CEP na criao do usurio
Em lib/my_app/users/create.ex
, vamos alterar a funo para passar a validar o CEP do usurio e, tambm, vamos pegar as informaes da cidade e UF. Esta funo apresenta um problema mas, vamos melhorar ela depois.
alias MyApp.ViaCep.HttpoisonClient, as: Client...@spec call(user_params()) :: {:error, Ecto.Changeset.t() | map()} | {:ok, Ecto.Schema.t()} def call(params) do cep = Map.get(params, :cep) with {:ok, %{"localidade" => city, "uf" => uf}} <- Client.get_cep_info(cep), params <- Map.merge(params, %{city: city, uf: uf}), changeset <- User.changeset(%User{}, params), {:ok, %User{}} = user <- Repo.insert(changeset) do user end end
Criando um usurio:
iex(1)> user_params = %{ ...(1)> first_name: "Mike",...(1)> last_name: "Wazowski",...(1)> cep: "95270000",...(1)> email: "mike_wazowski@monstros_sa.com"...(1)> }%{ cep: "95270000", email: "mike_wazowski@monstros_sa.com", first_name: "Mike", last_name: "Wazowski"}iex(2)> MyApp.Users.Create.call user_params CEP: "95270000"{:ok, %MyApp.User{ __meta__: #Ecto.Schema.Metadata<:loaded, "users">, cep: "95270000", city: "Flores da Cunha", email: "mike_wazowski@monstros_sa.com", first_name: "Mike", id: 1, inserted_at: ~N[2022-05-15 21:38:14], last_name: "Wazowski", uf: "RS", updated_at: ~N[2022-05-15 21:38:14] }}
CEP Invlido
iex(1)> user_params = %{ ...(1)> first_name: "Mike",...(1)> last_name: "Wazowski",...(1)> cep: "123",...(1)> email: "mike_wazowski@monstros_sa.com"...(1)> }%{ cep: "123", email: "mike_wazowski@monstros_sa.com", first_name: "Mike", last_name: "Wazowski"}iex(2)> MyApp.Users.Create.call user_params CEP: "123"{:error, %{result: "Invalid CEP!", status: :bad_request}}
Perceba que foi mostrada a mensagem Invalid CEP!
ao invs de mostrar a validao do changeset, alertando que o CEP devia possuir 8 caracteres, desta forma, teriamos a validao sem precisar fazer uma chamada desnecessria para a API externa.
Evitando chamadas desnecessarias para API Externa (apply_action)
Vamos agora validar todos os dados do changeset antes de fazer a chamada para a API do ViaCep.
Testando a validao do changeset sem usar o Repo.insert()
:
iex(1)> import Ecto.Changesetiex(2)> user_params = %{...(2)> first_name: "Mike",...(2)> last_name: "Wazowski",...(2)> cep: "123",...(2)> email: "mike_wazowski@monstros_sa.com"...(2)> }%{ cep: "123", email: "mike_wazowski@monstros_sa.com", first_name: "Mike", last_name: "Wazowski"}iex(3)> MyApp.User.changeset(%MyApp.User{}, user_params) |> apply_action(:create){:error, #Ecto.Changeset< action: :create, changes: %{ cep: "123", email: "mike_wazowski@monstros_sa.com", first_name: "Mike", last_name: "Wazowski" }, errors: [ cep: {"should be %{count} character(s)", [count: 8, validation: :length, kind: :is, type: :string]} ], data: #MyApp.User<>, valid?: false >}
Vamos alterar o arquivo lib/my_app/user.ex
, adicionando a funo validate_before_insert
:
def validate_before_insert(changeset), do: apply_action(changeset, :insert)
Vamos agora alterar o arquivo lib/my_app/users/create.ex
:
@spec call(user_params()) :: {:error, Ecto.Changeset.t() | map()} | {:ok, Ecto.Schema.t()} def call(params) do cep = Map.get(params, :cep) changeset = User.changeset(%User{}, params) with {:ok, %User{}} <- User.validate_before_insert(changeset), {:ok, %{"localidade" => city, "uf" => uf}} <- Client.get_cep_info(cep), params <- Map.merge(params, %{city: city, uf: uf}), changeset <- User.changeset(%User{}, params), {:ok, %User{}} = user <- Repo.insert(changeset) do user end end
Agora antes de fazer a chamada para o ViaCep estamos checando todas as validaes do do changeset
antes:
User-Agent header
Algumas API's externas podem solicitar algo a mais para realizar as chamadas, muitas podem pedir um token, no qual voc consegue na documentao, ou no caso da API do github, um User-Agent header
:
Com o Tesla facilmente voc pode usar o plug Tesla.Middleware.Headers
:
No Httpoison:
iex> HTTPoison.get "https://api.github.com/users/maiquitome/repos", [{"User-Agent", "foobar"}]
Leia na documentao do HTTPoison a parte de options.
Ento, fique atento a documentao da API na qual voc est realizando a chamada.
Concluso
Em algum momento voc vai precisar consumir dados de uma API externa; o que nos dias de hoje bem normal. Ler a documentao da API externa o primeiro passo para o sucesso. Em Elixir temos timas ferramentas de HTTP Client, a documentao dessas ferramentas tambm merece ser lida. Precisamos cuidar para no fazer chamadas desnecessrias em API's externas para no comprometer a performance da nossa aplicao. O prximo passo agora saber como realizar testes automatizados nessas chamadas externas; assunto para um prximo post.
Original Link: https://dev.to/maiquitome/elixir-consumindo-dados-de-uma-api-externa-3g78
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To