DEV Community

Cover image for 💧🍔 Projeto Rockelivery: API para Pedidos em um Restaurante com Elixir e Phoenix (Parte 3)
Dev Maiqui 🇧🇷
Dev Maiqui 🇧🇷

Posted on • Edited on

3 2

💧🍔 Projeto Rockelivery: API para Pedidos em um Restaurante com Elixir e Phoenix (Parte 3)

Essa é a terceira parte do projeto Rockelivery. Esse projeto faz parte do Bootcamp da Rocketseat, ministrado pelo professor Rafael Camarda.

Caso queira adquirir os cursos da Rocketseat com o meu cupom de desconto Acesse esse link

Conteúdo:

📇 Módulo para Leitura de Usuários
🔎 A Rota de Show dos Usuários
💀 A Rota de Deleção de Usuários
🔴 Centralizando as Mensagens de Erro
📝 Módulo para Update de Usuários
🔄 A Rota Update
🔌 Criando um Plug

A Jornada do Autodidata em Inglês

📇 Módulo para Leitura de Usuários

Vamos agora fazer um módulo para pesquisar os usuários no banco de dados.

Lembrando que podemos pegar todos os usuários do banco de dados usando a função Rockelivery.Repo.all():

iex> Rockelivery.Repo.all(Rockelivery.User)
[debug] QUERY OK source="users" db=22.5ms decode=3.6ms queue=3.2ms idle=705.1ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
[
  %Rockelivery.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    address: "Rua...",
    age: 28,
    cep: "12345678",
    cpf: "12345678910",
    email: "maiqui@teste.com",
    id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
    inserted_at: ~N[2021-06-05 14:45:28],
    name: "Maiqui",
    password: nil,
    password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
    updated_at: ~N[2021-06-05 14:45:28]
  },
...
Enter fullscreen mode Exit fullscreen mode

Ou buscar um usuário específico passando o ID desse usuário para a função Rockelivery.Repo.get(). Note que quando o usuário é encontrado, um Schema é retornado:

iex> Rockelivery.Repo.get(Rockelivery.User, "482f95a7-b447-42e9-ae67-aef72954c3f0")
[debug] QUERY OK source="users" db=2.9ms queue=2.4ms idle=1418.4ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 240>>]
%Rockelivery.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  address: "Rua...",
  age: 28,
  cep: "12345678",
  cpf: "12345678910",
  email: "maiqui@teste.com",
  id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
  inserted_at: ~N[2021-06-05 14:45:28],
  name: "Maiqui",
  password: nil,
  password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
  updated_at: ~N[2021-06-05 14:45:28]
}
Enter fullscreen mode Exit fullscreen mode

Mas quando o usuário não é encontrado, um nil é retornado:
image

Crie o arquivo lib/rockelivery/users/get.ex:

defmodule Rockelivery.Users.Get do
  alias Rockelivery.{Repo, User}

  def by_id(id) do
    case Repo.get(User, id) do
      nil -> {:error, %{status: :not_found, result: "User not found"}}
      user_schema -> {:ok, user_schema}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Testando com um ID inválido:

iex> Rockelivery.Users.Get.by_id("482f95a7-b447-42e9-ae67-aef72954c3f1")
[debug] QUERY OK source="users" db=4.6ms queue=0.1ms idle=1023.1ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 241>>]
{:error, %{result: "User not found", status: :not_found}}
Enter fullscreen mode Exit fullscreen mode

Testando com um ID válido:

iex> Rockelivery.Users.Get.by_id("482f95a7-b447-42e9-ae67-aef72954c3f0")
[debug] QUERY OK source="users" db=2.0ms queue=0.1ms idle=1622.8ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 240>>]
{:ok,
 %Rockelivery.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   address: "Rua...",
   age: 28,
   cep: "12345678",
   cpf: "12345678910",
   email: "maiqui@teste.com",
   id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
   inserted_at: ~N[2021-06-05 14:45:28],
   name: "Maiqui",
   password: nil,
   password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
   updated_at: ~N[2021-06-05 14:45:28]
 }}
Enter fullscreen mode Exit fullscreen mode

Se tentarmos usar um ID que não seja no formato UUID, receberemos uma exceção:
image

Então precisamos refatorar o nosso código para verificar se um ID é um UUID. Vamos antes só entender a função Ecto.UUID.cast/1:

iex> Ecto.UUID.cast("123456")
:error

iex> Ecto.UUID.cast("482f95a7-b447-42e9-ae67-aef72954c3f0")
{:ok, "482f95a7-b447-42e9-ae67-aef72954c3f0"}
Enter fullscreen mode Exit fullscreen mode

Em lib/rockelivery/users/get.ex:

defmodule Rockelivery.Users.Get do
  alias Ecto.UUID
  alias Rockelivery.{Repo, User}

  def by_id(id) do
    case UUID.cast(id) do
      :error -> {:error, %{status: :bad_request, result: "Invalid Format!"}}
      {:ok, uuid} -> get(uuid)
    end
  end

  defp get(id) do
    case Repo.get(User, id) do
      nil -> {:error, %{status: :not_found, result: "User not found!"}}
      user_schema -> {:ok, user_schema}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

🔎 A Rota de Show dos Usuários

A partir de agora vamos construir todo o código para que a rota show funcione. Lembrando que a rota já foi criada quando usamos o código resources:

image

image

Fachada para a função by_id

Vamos construir a nossa fachada para a função Rockelivery.Users.Get.by_id/1, já que precisaremos dela na action show do controller RockeliveryWeb.UsersController.

Em lib/rockelivery.ex:

defmodule Rockelivery do
  alias Rockelivery.Users.Create, as: UserCreate

  # adicione
  alias Rockelivery.Users.Get, as: UserGet

  defdelegate create_user(params), to: UserCreate, as: :call

  # adicione 
  defdelegate get_user_by_id(id), to: UserGet, as: :by_id
end
Enter fullscreen mode Exit fullscreen mode

Construindo a action show

Em lib/rockelivery_web/controllers/user_controller.ex adicione o código abaixo:

def show(conn, %{"id" => id}) do
  with {:ok, %User{} = user} <- Rockelivery.get_user_by_id(id) do
    conn
    |> put_status(:ok)
    |> render("show.json", user: user)
  end
end
Enter fullscreen mode Exit fullscreen mode

Ao contrário da action index, invés de params estamos fazendo Pattern Matching colocando %{"id" => id}.

Adicionando "show.json" na view

Em lib/rockelivery_web/views/users_view.ex adicione o código abaixo:

def render("show.json", %{user: %User{} = user}), do: %{user: user}
Enter fullscreen mode Exit fullscreen mode

Testando a nossa rota show

imageimage

Vamos passar um id com formato inválido para ver a mensagem:
image

Ajustando a mensagem

Em lib/rockelivery_web/views/error_view.ex adicione o código abaixo:

def render("error.json", %{result: %Changeset{} = changeset}) do
  %{message: translate_errors(changeset)}
end
# abaixo do código acima
# adicione
def render("error.json", %{result: error_message}) do
  %{message: error_message}
end
Enter fullscreen mode Exit fullscreen mode

Formato do ID inválido:
image

Formato válido, mas o ID não existe no banco:
image

Refatoração

Lembrando que antes do erro ir para a error_view, ele passa no FallbackController e nosso result que recebia só um changeset agora recebe também uma mensagem, então vamos alterar o nosso código. Podemos alterar o nome da variável changeset para result, ou para changeset_or_message que fica mais claro ao ser lido:
image

💀 A Rota de Deleção de Usuários

Vamos começar com o módulo Delete. Ele vai ser parecido com o módulo Get.

Módulo Delete

Crie um arquivo em lib/rockelivery/users/delete.ex, e o conteúdo deve ter essa aparência:

defmodule Rockelivery.Users.Delete do
  alias Ecto.UUID
  alias Rockelivery.{Repo, User}

  def call(id) do
    case UUID.cast(id) do
      :error -> {:error, %{status: :bad_request, result: "Invalid Format!"}}
      {:ok, uuid} -> delete(uuid)
    end
  end

  defp delete(id) do
    case Repo.get(User, id) do
      nil -> {:error, %{status: :not_found, result: "User not found!"}}
      user_schema -> Repo.delete(user_schema)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Vamos agora fazer a fachada para este módulo.

Delete Facade

Em lib/rockelivery.ex adicione o código abaixo:

alias Rockelivery.Users.Delete, as: UserDelete

defdelegate delete_user(id), to: UserDelete, as: :call
Enter fullscreen mode Exit fullscreen mode

Delete Action

Em lib/rockelivery_web/controllers/users_controller.ex adicione o código abaixo:

def delete(conn, %{"id" => id}) do
    with {:ok, %User{}} <- Rockelivery.delete_user(id) do
      conn
      |> put_status(:no_content)
      |> text("")
    end
end
Enter fullscreen mode Exit fullscreen mode

Como estamos colocando um status :no_content, não vamos devolver nenhum body, por isso não vamos usar a função render, apenas devolveremos uma string vazia.

Testando a deleção

image

🔴 Centralizando as Mensagens de Erro

Agora existe uma oportunidade para refarorarmos o nosso código. Podemos observar que começamos a repetir as mensagens de erro e os status code:

image

Vamos então centralizar essas menssagens em um arquivo só.

Criando um arquivo para as mensagens de erro

Crie lib/rockelivery/error.ex e o seu conteúdo deve ter essa aparência:

defmodule Rockelivery.Error do
  @keys [:status, :result]

  @enforce_keys @keys

  defstruct @keys

  def build(status, result) do
    %__MODULE__{
      status: status,
      result: result
    }
  end

  def build_user_not_found, do: build(:not_found, "User not found")
  def build_invalid_id_format, do: build(:bad_request, "Invalid id format")
end
Enter fullscreen mode Exit fullscreen mode

Refatorando

Em lib/rockelivery/users/get.ex:

image

Faça o mesmo em lib/rockelivery/users/delete.ex:
image

E, em lib/rockelivery/users/create.ex use apenas a função Error.build/2:

defp handle_insert({:error, changeset}) do
    # ANTES
    # {:error, %{status: :bad_request, result: changeset}}

    # DEPOIS
    {:error, Error.build(:bad_request, changeset)}
end
Enter fullscreen mode Exit fullscreen mode

Vamos alterar também o FallbackController e colocar agora a nossa struct error. Com ela nosso código fica mais declarativo, ficando mais fácil entender o código.

Em lib/rockelivery_web/controllers/fallback_controller.ex:

image

📝 Módulo para Update de Usuários

Primeiramente vamos verificar na documentação como funciona o callback Ecto.Repo.update: https://hexdocs.pm/ecto/Ecto.Repo-callback-update.html

image

No exemplo da documentação podemos ver que precisaremos de um changeset:
image

Vamos criar o arquivo lib/rockelivery/users/update.ex:

defmodule Rockelivery.Users.Update do
  alias Ecto.UUID
  alias Rockelivery.{Error, Repo, User}

  def call(%{"id" => id} = params) do
    case UUID.cast(id) do
      :error -> {:error, Error.build_invalid_id_format()}
      {:ok, _uuid} -> update(params)
    end
  end

  defp update(%{"id" => id} = params) do
    case Repo.get(User, id) do
      nil -> {:error, Error.build_user_not_found()}
      user_schema -> do_update(user_schema, params)
    end
  end

  defp do_update(%User{} = user, %{} = params) do
    user
    |> User.changeset(params)
    |> Repo.update()
  end
end
Enter fullscreen mode Exit fullscreen mode

Precisamos alterar também lib/rockelivery/user.ex para podermos passar um schema como parâmetro:
image

Testando o Update

Buscando todos os usuários cadastrados com o comando:

iex> Rockelivery.Repo.all Rockelivery.User
Enter fullscreen mode Exit fullscreen mode

image

Pegamos um ID e vamos tentar atualizar o nome:

  • Criando os parâmetros:
iex> params = %{"id" => "482f95a7-b447-42e9-ae67-aef72954c3f0", "name" => "Maiqui Tomé"}
Enter fullscreen mode Exit fullscreen mode
  • Executando a função Update:
iex> Rockelivery.Users.Update.call params
Enter fullscreen mode Exit fullscreen mode
  • Resultado: image

Por que não funcionou?
image

Quando passamos o schema %User{} com os dados puxados do banco para a função User.changeset/2 estamos passando com o campo password nulo, pois o banco de dados não retorna os dados dele já que esse campo é virtual. A função User.changeset/2 faz a validação e diz que esse campo é obrigatório informar. Precisamos refatorar novamente o nosso código.

Em lib/rockelivery/user.ex:

image

Em lib/rockelivery/users/update.ex:

image

Resultado:
image

🔄 A Rota Update

Após o módulo de update criado, vamos partir para a criação de todo o código para a rota update funcionar.

Update Facade

Em lib/rockelivery.ex adicione o código abaixo:

alias Rockelivery.Users.Update, as: UserUpdate

defdelegate update_user(params), to: UserUpdate, as: :call
Enter fullscreen mode Exit fullscreen mode

Update Action

Em lib/rockelivery_web/controllers/user_controller.ex adicione o código abaixo:

def show(conn, %{"id" => id}) do
  with {:ok, %User{} = user} <- Rockelivery.get_user_by_id(id) do
      conn
      |> put_status(:ok)
      # alterando "show.json" para "user.json"
      |> render("user.json", user: user)
  end
end

# adicione
def update(conn, %{} = params) do
  with {:ok, %User{} = user} <- Rockelivery.update_user(params) do
      conn
      |> put_status(:ok)
      |> render("user.json", user: user)
  end
end
Enter fullscreen mode Exit fullscreen mode

Como a action show é praticamente igual a action update vamos usar o template "user.json" para os dois. Vamos alterar agora o código da view.

Em lib/rockelivery_web/views/users_view.ex altere "show.json" para "user.json":
image

Testando a Rota Update

Primeiramente, vamos pesquisar um usuário usando a action show:
image
Para todos os campos aparecerem certifique-se que você acrescentou eles no Jason.Encoder:
image

Agora vamos editar esse usuário usando a rota nova de update. Vou apenas trocar o nome de "Maiqui Tomé" para "Maiqui Pirolli Tomé":
image

Ao pesquisar novamente usando a rota Get:
image

🔌 Criando um Plug

Primeiramente, antes de entendermos melhor o que é um Plug, vamos entender porque vamos precisar de um. Podemos perceber na imagem abaixo, que em vários arquivos estamos repetindo o mesmo trecho de código para verificar se um ID é um UUID.

image

Para melhorar o nosso código precisamos construir um Plug. Vamos antes enteder ele melhor.

Plug é uma convenção para manipular a struct de conexão %Plug.Conn{}. Mesmo não fazendo parte do núcleo de Elixir, Plug é um projeto oficial de Elixir. Você pode conferir a documentação oficial do Plug aqui: https://hexdocs.pm/plug/readme.html

A definição da documentação do Phoenix diz que o Plug é uma especificação para módulos combináveis ​​entre aplicativos da web. É também uma camada de abstração para adaptadores de conexão de diferentes servidores da web. A ideia básica do Plug é unificar o conceito de uma "conexão" na qual operamos. Isso difere de outras camadas de middleware HTTP, como Rack do Ruby on Rails, onde a solicitação e a resposta são separadas na pilha de middleware. Essa definição e mais detalhes você encontra aqui: https://hexdocs.pm/phoenix/plug.html

Function Plugs

Para atuar como um Plug, uma função precisa aceitar uma estrutura de conexão (%Plug.Conn{}) e opções. Ela também precisa retornar uma estrutura de conexão. Qualquer função que atenda a esses critérios servirá.

image

Veja mais detalhes aqui: https://hexdocs.pm/phoenix/plug.html#function-plugs

Module Plugs

Os Module Plugs são outro tipo de Plug que nos permite definir uma transformação de conexão em um módulo. O módulo só precisa implementar duas funções:

  1. init/1 que inicializa quaisquer argumentos ou opções a serem passadas para call/2
  2. call/2 que realiza a transformação da conexão. call/2 é apenas uma Function Plug que vimos anteriormente.

Entendendo o fluxo

Um Plug executa antes da request chegar no controller. Vamos tentar entender melhor esse fluxo no arquivo de rotas.

Vamos pensar que um usuário fez um post. O navegador acessou a barra de endereço http://localhost:4000/api/users, enviou um HTTP Request para a nossa aplicação que estava executando nesse endereço. O HTTP Request foi construído com um par do verbo POST e o caminho /Users, que foi mapeado para um par do controller UsersController e a action create.

Antes de chegar na action create do UsersController:

  1. Primeiramente vai para o scope "/api";
  2. depois para o pipe_throw :api que redireciona para o bloco pipeline :api;
  3. depois para o plug :accepts, ["json"] dizendo que essa rota aceita o formato json,
  4. depois no nosso plug UUIDChecker que criaremos ainda,
  5. e só depois para o UsersController onde os dados serão moldados na action create, nesse caso.

image

O nosso primeiro plug

Crie um arquivo no diretório lib/rockelivery_web/plugs/uuid_checker.ex. O conteúdo deste arquivo deve ter a seguinte aparência:

defmodule RockeliveryWeb.Plugs.UUIDChecker do
  import Plug.Conn

  alias Ecto.UUID
  alias Plug.Conn

  # init/1 inicializa quaisquer argumentos ou opções a serem passadas para call/2
  def init(options), do: options

  # call/2 é uma Function Plug que realiza a transformação da conexão.
  # Toda Function Plug precisa aceitar uma estrutura de conexão %Plug.Conn{} e opções.
  def call(%Conn{params: %{"id" => id}} = conn, _opts) do
    case UUID.cast(id) do
      :error -> render_error(conn)
      # se o ID for um UUID a conexão (conn) pode ir para o controller:
      {:ok, _uuid} -> conn
    end
  end

  # Caso não tenha id nos paramêtros, como na rota create,
  # continua enviando a conexão no fluxo normal
  def call(conn, _opts), do: conn

  defp render_error(conn) do
    body = Jason.encode!(%{message: "Invalid UUID"})

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(:bad_request, body)
    |> halt()
  end
end
Enter fullscreen mode Exit fullscreen mode

Vamos entender algumas funções usadas no código acima:

Em lib/rockelivery_web/router.ex:

image

Refatorando

Em lib/rockelivery/error.view podemos retirar a mensagem de erro do UUID, já que isso está sendo tratado agora no nosso Plug que criamos:
image

Podemos agora retirar a verificação do UUID nos 3 arquivos onde estava sendo usado:
image

Exemplo da refatoração no arquivo de update:
image
Faça essa refatoração nos outros 2 arquivos.

Agora nosso Plug está tratando os IDs inválidos :)
image

E assim terminamos a terceira parte da nossa aplicação :)

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay