💧🔗 Elixir: Consumindo dados de uma API externa
Tags: elixir, braziliandevs, api
Published: 2022-05-16 00:21:39

Neste post vamos aprender a consumir dados de uma API externa. No momento da criação do usuário, vamos realizar uma chamada na API do **ViaCep** para validar o CEP dele, buscando e gravando as informações de cidade e UF desse usuário. Vamos aprender também como evitar fazer chamadas desnecessárias.

* [Criando o projeto](#criando-o-projeto)
 - [Tabela users](#tabela-users)
 - [Função para criar um usuário](#user-create)
* [Consumindo dados de uma API externa](#consumindo)
 - [Instalando o Tesla](#instalando-o-tesla)
 - [Instalando o HTTPoison](#instalando-o-httpoison)
 - [HTTP Client com Tesla](#http-client-tesla)
 - [HTTP Client com HTTPoison](#http-client-httpoison)
 - [Validando o CEP na criação do usuário](#validando-cep)
 - [Evitando chamadas desnecessarias para API Externa (apply_action)](#evitando-chamadas-desnecessarias)
 - [User-Agent header](#user-agent) 
* [Conclusão](#conclusao)

[![A Jornada do Autodidata em Inglês](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pkoqkmh9hic17tnddjf6.png)](https://go.hotmart.com/R93865620U)

# Criando o projeto <a name="criando-o-projeto">

```elixir
$ mix phx.new learning_external_api --app my_app
```

Link do projeto pronto: https://github.com/maiquitome/learning_external_api




# Tabela users <a name="tabela-users">

Criando a migration e o schema:
```bash
$ 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`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2msvbo0148djdboiwnkr.png)
 
 

Criando o banco de dados e rodando as migrations:

```bash
$ mix ecto.setup
```




# Função para criar um usuário <a name="user-create">

Precisaremos de uma função para criar um usuário, pois vamos usar a API do ViaCEP para validar o CEP dele. Vamos adicionar essa validação posteriormente.

Em `lib/my_app/users/create.ex`:

```elixir
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()
  end
end
```




# Consumindo dados de uma API externa <a name="consumindo">

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 <a name="instalando-o-tesla">

Para instalar o Tesla, adicione ao `mix.exs`:
```elixir
defp deps do
  [
    {:tesla, "~> 1.4"},

    # opcional, mas recomendado
    {:hackney, "~> 1.17"}

    # esse não precisa pois já vem com o phoenix
    {:jason, ">= 1.0.0"}
  ]
end
```
Você pode encontrar a versão mais recente do Tesla em: https://hex.pm/packages/tesla

Instalando as dependências:
```bash
$ mix deps.get
```
Testando o Tesla:
```bash
$ iex -S mix
```
```elixir
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 <a name="instalando-o-httpoison">

Para instalar o HTTPoison, adicione ao `mix.exs`:
```elixir
defp deps do
  [
    {:httpoison, "~> 1.8"}
  ]
end
```
Você pode encontrar a versão mais recente do HTTPoison em: https://hex.pm/packages/httpoison

Instalando as dependências:
```bash
$ mix deps.get
```
Testando o HTTPoison:
```bash
$ iex -S mix
```

```elixir
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}}
```




# HTTP Client com Tesla <a name="http-client-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`:

```elixir
defmodule MyApp.ViaCep.TeslaClient do
  # Ao invés de Tesla.get(), vc vai usar apenas get()
  use Tesla

  alias Tesla.Env

  @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}}
  end
end
```

Vamos fazer o teste:

```elixir
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 <a name="http-client-httpoison">

No HTTPoison não temos um middleware pronto para transformar o `json` em `map`, então vamos precisar usar o `Jason.decode()` que já vem instalado no Phoenix.

Em `lib/my_app/via_cep/httpoison_client.ex`:

```elixir
defmodule MyApp.ViaCep.HttpoisonClient do
  alias HTTPoison.{Error, Response}

  @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}}
  end
end

```

Vamos fazer o teste:

```elixir
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 criação do usuário <a name="validando-cep">

Em `lib/my_app/users/create.ex`, vamos alterar a função para passar a validar o CEP do usuário e, também, vamos pegar as informações da cidade e UF. Esta função apresenta um problema mas, vamos melhorar ela depois.

```elixir
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 usuário:
```elixir
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 Inválido

```elixir
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 invés de mostrar a validação do changeset, alertando que o CEP devia possuir 8 caracteres, desta forma, teriamos a validação sem precisar fazer uma chamada desnecessária para a API externa.


# Evitando chamadas desnecessarias para API Externa (apply_action) <a name="evitando-chamadas-desnecessarias">

Vamos agora validar todos os dados do changeset antes de fazer a chamada para a API do ViaCep.


Testando a validação do changeset sem usar o `Repo.insert()`:

```elixir
iex(1)> import Ecto.Changeset

iex(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
 >}
```

O [`apply_action`](https://hexdocs.pm/ecto/Ecto.Changeset.html#apply_action/2) aplica a ação changeset somente se as alterações forem válidas. 

Se as alterações forem válidas, todas as alterações serão aplicadas aos dados do changeset. Se as alterações forem inválidas, nenhuma alteração será aplicada e uma tupla de erro será retornada com o conjunto de alterações contendo a ação que tentou ser aplicada.

A ação pode ser qualquer átomo.

Vamos alterar o arquivo `lib/my_app/user.ex`, adicionando a função `validate_before_insert`:

```elixir
def validate_before_insert(changeset), do: apply_action(changeset, :insert)
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/84731lz1o7ymsfw65at7.png)
 
 

Vamos agora alterar o arquivo `lib/my_app/users/create.ex`:

```elixir
@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 validações do do `changeset` antes:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/npgjz56n97frjqwgai1v.png)



# User-Agent header <a name="user-agent">

Algumas API's externas podem solicitar algo a mais para realizar as chamadas, muitas podem pedir um token, no qual você consegue na documentação, ou no caso da API do github, um `User-Agent header`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ib0gsyv8cowk8iulw6w5.png)
 
Com o Tesla facilmente você pode usar o `plug Tesla.Middleware.Headers`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bsrld4xmhy0u6ww4t1t3.png) 

No Httpoison:

```elixir
iex> HTTPoison.get "https://api.github.com/users/maiquitome/repos", [{"User-Agent", "foobar"}]
```

Leia na documentação do HTTPoison a parte de [options](https://hexdocs.pm/httpoison/readme.html#options).

Então, fique atento a documentação da API na qual você está realizando a chamada.


# Conclusão <a name="conclusao">


Em algum momento você vai precisar consumir dados de uma API externa; o que nos dias de hoje é bem normal. Ler a documentação da API externa é o primeiro passo para o sucesso. Em Elixir temos ótimas ferramentas de HTTP Client, a documentação dessas ferramentas também merece ser lida. Precisamos cuidar para não fazer chamadas desnecessárias em API's externas para não comprometer a performance da nossa aplicação. O próximo passo agora é saber como realizar testes automatizados nessas chamadas externas; assunto para um próximo post.