DEV Community

loading...
Cover image for Projeto Rockelivery: API para Pedidos em um Restaurante com Elixir e Phoenix (Parte 1)

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

Maiqui Tomé
Junior Backend Developer
Updated on ・11 min read

O projeto Rockelivery faz parte do Bootcamp da Rocketseat, ministrado pelo professor Rafael Camarda.

Eu usei também alguns trechos da documentação oficial do Ecto e algumas explicações do site Elixir School. Existe um guia bem bacana sobre Ecto e em português que você pode estudar também: https://elixirschool.com/pt/lessons/ecto/basics/

Para você poder acompanhar a construção deste projeto é preciso ter o conhecimento básico da Linguagem Elixir :)

Acompanhe o repositório no GitHub: https://github.com/maiquitome/rockelivery_api

Conteúdo:

📚 O que vamos aprender?

  • Instalar dependências
  • Criar rotas (CRUD completo)
    • create
    • read
    • update
    • delete
  • Criar Plugs
  • Entender o Ecto
    • interagir com bancos de dados
    • migration
    • schema
    • changeset
  • Testar uma aplicação Phoenix

❔ Como vai ser o projeto?

  • Usuário pode se cadastrar e gerenciar a conta dele
  • Items do restaurante podem ser cadastrados
  • Usuário pode realizar pedidos
    • Para realizar pedidos, o usuário tem que se logar
    • O CEP do usuário deve ser válido, então vamos usar uma API para validar o CEP e aprender como realizar chamadas HTTP
  • Semanalmente, um relatório de pedidos por usuário deve ser gerado, para isso vamos usar GenServers.

🔧 Setup inicial do projeto

Se você ainda não tem o Elixir, Phoenix e o PostgreSQL instalados, você pode acessar outro artigo meu onde explico sobre a instalação deles: Instalação das ferramentas de uma pessoa desenvolvedora Elixir

Comando para criar o projeto:

$ mix phx.new rockelivery --no-webpack --no-html
Enter fullscreen mode Exit fullscreen mode

Após esse comando, ele nos perguntará se queremos instalar as dependências. Vamos dizer que sim digitando y e apertando a tecla ENTER.

Fetch and install dependencies? [Yn] y
* running mix deps.get
* running mix deps.compile
...
Enter fullscreen mode Exit fullscreen mode

Por que temos que usar as opções --no-webpack e --no-html?

  • Porque não vamos fazer front-end (HTML, CSS, JavaScript);
  • E, porque vamos construir apenas uma API JSON.

Se fossemos usar só para front-end usaríamos a opção --no-ecto, assim as configurações e arquivos para banco de dados não seriam gerados. Você pode encontrar outras opções aqui: https://hexdocs.pm/phoenix/Mix.Tasks.Phx.New.html

Entre na pasta do projeto com o comando:

$ cd rockelivery
Enter fullscreen mode Exit fullscreen mode

👦 Criando a Migração e a Tabela do Usuário

Para criar e modificar tabelas no banco de dados, utilizamos as migrações do Ecto. Cada migração descreve uma série de ações para serem realizadas no nosso banco, como quais tabelas criar ou atualizar.

Você pode estar se perguntando agora, mas o que é esse tal de Ecto? Pois bem, vou te responder agora, o Ecto é um projeto oficial do Elixir que fornece uma camada de banco de dados e linguagem integrada para consultas. Com o Ecto podemos criar migrações, definir esquemas, inserir e atualizar registros, e fazer consultas no banco de dados.

A convenção no Ecto é pluralizar o nome das tabelas, portanto, vamos nomear a nossa tabela como users. Para criar a migração vamos rodar o comando abaixo:

$ mix ecto.gen.migration create_users_table
Enter fullscreen mode Exit fullscreen mode

Esse comando irá gerar um novo arquivo na pasta priv/repo/migrations contendo uma timestamp no nome. Se navegarmos para esse diretório e abrirmos a migração, veremos algo assim:

Em priv/repo/migrations/"número-da-timestamp"_create_users_table.exs:

defmodule Rockelivery.Repo.Migrations.CreateUsersTable do
  use Ecto.Migration

  def change do

  end
end
Enter fullscreen mode Exit fullscreen mode

A timestamp é importante para o Ecto saber em qual ordem ele precisa criar as tabelas.

Vamos criar a nossa tabela dentro da função change. Estar dentro da função change é importante para o Ecto criar a tabela quando rodarmos o comando de create ou desfazer automaticamente o que criamos caso rodemos o comando de rollback.

Existem também as funções up/0 e down/0 onde especificamos com detalhes o que queremos criar no up/0 e o que queremos desfazer no down/0. Mas ter que escrever as funções up/0 e down/0 para cada migração é entediante e sujeito a erros. Por curiosidade, vou deixar o link da documentação explicando mais sobre, mas por aqui vamos deixar tudo dentro do change mesmo.

up e down: https://hexdocs.pm/ecto_sql/Ecto.Migration.html

Vamos criar a nossa tabela User com os campos abaixo:

  • ID
  • address
  • age
  • cep
  • CPF
  • email
  • name
  • password

Em priv/repo/migrations/número-da-timestamp_create_users_table.exs:

defmodule Rockelivery.Repo.Migrations.CreateUsersTable do
  use Ecto.Migration

  def change do
    # A convenção no `Ecto` é pluralizar o nome das tabelas,
    # portanto, vamos nomear a nossa tabela como :users.
    create table :users do
      # o id é gerado automaticamente então não precisamos criar
      add :address,        :string
      add :age,            :integer
      add :cep,            :string
      add :cpf,            :string
      add :email,          :string
      add :name,           :string
      add :password_hash,  :string
      # Precisamos do password nomeado como password_hash 
      # pois vamos criptografar a senha do usuário...

      # essa função de timestamps gera automaticamente os campos
      # inserted_at para guardar a hora de criação do registro
      # e updated_at para guardar a hora da atualização do registro
      timestamps()
    end

    # O índice unique não deixará o cpf do usuário se repetir
    create unique_index(:users, [:cpf])
    # E também não deixará o email do usuário se repetir,
    create unique_index(:users, [:email])
    # ou seja, esses dois campos serão únicos


    # Por curiosidade, existe também esse formato:
    # create index("users", [:email], unique: true)
  end
end
Enter fullscreen mode Exit fullscreen mode

Alterando ID inteiro para UUID

Em aplicações Phoenix o ID é gerado automaticamente por padrão como inteiro, mas podemos modificar para que seja gerado automaticamente no formato UUID4.

O que é um UUID?

Um identificador único universal (do inglês universally unique identifier - UUID) é um número de 128 bits usado para identificar informações em sistemas de computação e dificilmente um UUID se repete.

Exemplo de UUID4: 297b4329-2982-4e6d-b2b1-6cf4b0c7a351

Gerador de UUID: https://www.uuidgenerator.net/

Vamos colocar as configurações no arquivo config/config.exs

# abaixo desse código:
config :rockelivery,
  ecto_repos: [Rockelivery.Repo]

# vamos colocar esse código:
config :rockelivery, Rockelivery.Repo,
  migration_primary_key: [type: :binary_id],
  migration_foreign_key: [type: :binary_id]
Enter fullscreen mode Exit fullscreen mode
  • :rockelivery é o nome da nossa app
  • Rockelivery.Repo é o modulo que conversa com o banco de dados
  • migration_primary_key é a chave primaria
  • migration_foreign_key é a chave estrangeira
  • :binary_id é um UUID na versão 4

DICA: Podemos usar a opção --binary-id junto do comando que criamos o projeto. Desta forma, ele irá gerar automaticamente o código para configurar os IDs como UUID.

Agora vamos criar a nossa tabela no banco de dados rodando o comando:

$ mix ecto.migrate
Enter fullscreen mode Exit fullscreen mode

Após, você deverá ver algo semelhante no terminal:

18:35:08.572 [info]  == Running 20210525185314 Rockelivery.Repo.Migrations.CreateUsersTable.change/0 forward

18:35:08.575 [info]  create table users

18:35:08.583 [info]  create index users_cpf_index

18:35:08.585 [info]  create index users_email_index

18:35:08.589 [info]  == Migrated 20210525185314 in 0.0s
Enter fullscreen mode Exit fullscreen mode

Podemos verificar no pgAdmim que a tabela foi criada com sucesso:
image

🧑🏻 Criando o Schema do User

Um esquema é um módulo que define um mapeando dos campos de uma tabela.

Enquanto nas tabelas utilizamos o plural, no esquema tipicamente se utiliza o singular. Então, criaremos um esquema User para a nossa tabela.

No diretório das regras de negócio lib/rockelivery, vamos criar o arquivo user.ex, e o código ficará assim:

defmodule Rockelivery.User do
  use Ecto.Schema

  # Como alteramos o tipo da chave primária,
  # precisamos configurar ela aqui,
  # usando o atributo de esquema @primary_key
  # podemos informar que temos um campo :id,
  # do tipo UUID (:binary_id)
  # e que deve ser gerado automaticamente.
  @primary_key {:id, :binary_id, autogenerate: true}

  # "users" no plural é nome da tabela
  schema "users" do
    field :address, :string
    field :age, :integer
    field :cep, :string
    field :cpf, :string
    field :email, :string
    field :name, :string
    field :password_hash, :string

    timestamps()
  end
end
Enter fullscreen mode Exit fullscreen mode

@primary_key - configura a chave primária do esquema. Ele espera uma tupla {field_name, type, options} com o nome do campo da chave primária, tipo (normalmente :id ou :binary_id, mas pode ser qualquer tipo) e opções. Leia mais sobre atributos de esquemas: https://hexdocs.pm/ecto/Ecto.Schema.html#module-schema-attributes

Agora que já temos o nosso esquema configurado, podemos perceber que um esquema (schema) nada mais é do que uma struct com metadados e, de maneira similar, podemos atualizar nossos esquemas como poderíamos fazer com qualquer outro map ou struct em Elixir:

Executando iex -S mix no terminal podemos fazer os testes:

iex> user = %Rockelivery.User{}
%Rockelivery.User{
  __meta__: #Ecto.Schema.Metadata<:built, "users">,
  address: nil,
  age: nil,
  cep: nil,
  cpf: nil,
  email: nil,
  id: nil,
  inserted_at: nil,
  name: nil,
  password_hash: nil,
  updated_at: nil
}

iex> user = %{user | age: 28}
%Rockelivery.User{
  __meta__: #Ecto.Schema.Metadata<:built, "users">,
  address: nil,
  age: 28,
  cep: nil,
  cpf: nil,
  email: nil,
  id: nil,
  inserted_at: nil,
  name: nil,
  password_hash: nil,
  updated_at: nil
}

iex> user.age
28
Enter fullscreen mode Exit fullscreen mode

DICA: Podemos criar o arquivo de migração juntamente com o do esquema executando o gerador do Phoenix:

$ mix phx.gen.schema User users address age:integer cep CPF email name password_hash
Enter fullscreen mode Exit fullscreen mode

Você pode ver mais detalhes na documentação: https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Schema.html#content

✔️ Usando Changesets

Existe um maneira melhor para alterarmos os dados em um schema. Usando um Changeset (Conjunto de Mudanças) conseguimos alterar os dados fazendo validações. Você pode obter mais detalhes visitando a documentação: https://hexdocs.pm/ecto/Ecto.Changeset.html#module-the-ecto-changeset-struct

Em lib/rockelivery/user.ex:

defmodule Rockelivery.User do
  use Ecto.Schema
  import Ecto.Changeset

  # variáveis de módulo (Module Attributes) começam com @
  # essas variáveis só podem ser usadas dentro do módulo

  # essa variável guarda os campos que são permitidos
  # para serem alterados  
  @fields_that_can_be_changed [
    :address,
    :age,
    :cep,
    :cpf,
    :email,
    :name,
    :password_hash
  ]

  # essa variável guarda os campos obrigatórios
  # que devem ser preenchidos
  @required_fields [
    :address,
    :age,
    :cep,
    :cpf,
    :email,
    :name,
    :password_hash
  ]

  @primary_key {:id, :binary_id, autogenerate: true}

  schema "users" do
    field :address, :string
    field :age, :integer
    field :cep, :string
    field :cpf, :string
    field :email, :string
    field :name, :string
    field :password_hash, :string

    timestamps()
  end

  # Nesta função changeset vamos criar um changeset com a função cast
  # e validar os campos obrigatórios com validate_required
  def changeset(%{} = params) do
    # %__MODULE__{} é igual a %Rockelivery.User{} que é o nosso Schema
    %__MODULE__{}
    # Ecto.Changeset.cast(Schema, %{} = params, [] = lista_de_campos_permitidos_a_serem_alterados)
    # O cast retorna o changeset
    |> cast(params, @fields_that_can_be_changed)
    # Ecto.Changeset.validate_required(changeset, [] = campos_que_devem_ser_preenchidos)
    |> validate_required(@required_fields)
  end
end
Enter fullscreen mode Exit fullscreen mode

Enquanto a função Ecto.Changeset.cast é usada para trabalhar com dados externos, existe a função Ecto.Changeset.change usada para trabalhar com dados internos da aplicação.

Elas são semelhantes, mas a função Ecto.Changeset.change é útil para alterar diretamente uma struct sem realizar castings (conversão de tipos) ou validações. Ao contrário da função cast, na change não temos a possibilidade de não permitir que certo dado seja alterado.

Entendendo mais sobre casting (conversão de tipos):

Nós definimos que o campo age é do tipo inteiro mas ao usar a função change no lugar do cast isso é ignorado:
image

image

Usando a função cast, ela só aceitará um tipo inteiro:
image

Se colocarmos um número como string ela fará a conversão (casting) para inteiro automaticamente:
image

Veja mais sobre a função change: https://hexdocs.pm/ecto/Ecto.Changeset.html#change/2

Veja mais sobre a função cast:
https://hexdocs.pm/ecto/Ecto.Changeset.html#cast/4

Changeset Válido

Agora vamos preencher todos os campos para deixar o changeset totalmente válido:

image

Validações e Restrições

Os changesets do Ecto fornecem validações e restrições (constraints) que acabam se transformando em erros caso algo dê errado.

A diferença entre elas é que a maioria das validações pode ser executada sem a necessidade de interagir com o banco de dados e, portanto, são sempre executados antes de tentar inserir ou atualizar a entrada no banco de dados. Algumas validações podem acontecer no banco de dados, mas são inerentemente inseguras. Essas validações começam com um prefixo unsafe_, como unsafe_validate_unique/3.

Por outro lado, as constraints dependem do banco de dados e estão sempre seguras. Como consequência, as validações são sempre verificadas antes das constraints. As constraints nem mesmo serão verificadas em caso de falha nas validações.

Aqui você pode encontrar várias funções para validação:
https://hexdocs.pm/ecto/Ecto.Changeset.html#module-external-vs-internal-data

image

Vamos adicionar novas validações para o nosso changeset.

Em lib/rockelivery/user.ex:

def changeset(%{} = params) do
    %__MODULE__{}
    |> cast(params, @fields_that_can_be_changed)
    |> validate_required(@required_fields)
    |> validate_length(:password_hash, min: 6)
    |> validate_length(:cep, is: 8)
    |> validate_length(:cpf, is: 11)
    # idade deve ser maior ou igual a 18
    |> validate_number(:age, greater_than_or_equal_to: 18)
    # email deve conter um caractere @
    |> validate_format(:email, ~r/@/)
    |> unique_constraint(:email)
    |> unique_constraint(:cpf)
end
Enter fullscreen mode Exit fullscreen mode

Executando iex -S mix no terminal, podemos verificar as validações:

iex> changeset = Rockelivery.User.changeset(%{age: "1", address: "rua..", cep: "989", cpf: "212", email: "email", name: "teste", password_hash: "123"})
#Ecto.Changeset<
  action: nil,
  changes: %{
    address: "rua..",
    age: 1,
    cep: "989",
    cpf: "212",
    email: "email",
    name: "teste",
    password_hash: "123"
  },
  errors: [
    email: {"has invalid format", [validation: :format]},
    age: {"must be greater than or equal to %{number}",
     [validation: :number, kind: :greater_than_or_equal_to, number: 18]},
    cpf: {"should be %{count} character(s)",
     [count: 11, validation: :length, kind: :is, type: :string]},
    cep: {"should be %{count} character(s)",
     [count: 8, validation: :length, kind: :is, type: :string]},
    password_hash: {"should be at least %{count} character(s)",
     [count: 6, validation: :length, kind: :min, type: :string]}
  ],
  data: #Rockelivery.User<>,
  valid?: false
>
Enter fullscreen mode Exit fullscreen mode

Encriptando a Senha

Primeiramente precisaremos ter a bibliteca externa Argon2. Você pode obter informações de como instalar ela aqui: Instalação das ferramentas de uma pessoa desenvolvedora Elixir

Em lib/rockelivery/user.ex:

defmodule Rockelivery.User do
  use Ecto.Schema
  import Ecto.Changeset

  @fields_that_can_be_changed [
    :address,
    :age,
    :cep,
    :cpf,
    :email,
    :name,
    # altere password_hash para password.
    # o campo password_hash pode ser retirado
    # dos campos permitidos pois será utilizada
    # a função change para alterar o password_hash
    :password
  ]

  @required_fields [
    :address,
    :age,
    :cep,
    :cpf,
    :email,
    :name,
    # altere password_hash para password.
    # o password_hash só será alterado após as validações,
    # por isso deve ser retirado dos campos obrigatórios.
    :password
  ]

  @primary_key {:id, :binary_id, autogenerate: true}

  schema "users" do
    field :address, :string
    field :age, :integer
    field :cep, :string
    field :cpf, :string
    field :email, :string
    field :name, :string
    # adicionando um campo virtual
    # o valor dele não será gravado no banco de dados
    # será apenas para capturar a senha do usuário
    field :password, :string, virtual: true
    field :password_hash, :string

    timestamps()
  end

  def changeset(%{} = params) do
    %__MODULE__{}
    |> cast(params, @fields_that_can_be_changed)
    |> validate_required(@required_fields)
    |> validate_length(:password_hash, min: 6)
    |> validate_length(:cep, is: 8)
    |> validate_length(:cpf, is: 11)
    |> validate_number(:age, greater_than_or_equal_to: 18)
    |> validate_format(:email, ~r/@/)
    |> unique_constraint(:email)
    |> unique_constraint(:cpf)
    # adicione a função para encriptar a senha
    |> put_pass_hash()
  end

  defp put_pass_hash(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do
    change(changeset, Argon2.add_hash(password))
  end

  # caso o changeset seja 'valid?: false'
  defp put_pass_hash(changeset), do: changeset
end
Enter fullscreen mode Exit fullscreen mode

Executando iex -S mix no terminal, podemos verificar que a função Argon2.add_hash preencheu o campo password_hash encriptando o valor de password:

iex> changeset = Rockelivery.User.changeset(%{age: 28, address: "rua..", cep: "12345678", cpf: "12345678910", email: "maiqui@email.com", name: "Maiqui", password: "123456"})
#Ecto.Changeset<
  action: nil,
  changes: %{
    address: "rua..",
    age: 28,
    cep: "12345678",
    cpf: "12345678910",
    email: "maiqui@email.com",
    name: "Maiqui",
    password: "123456",
    password_hash: "$argon2id$v=19$m=131072,t=8,p=4$ifRuVbsF29358ZZTbdYxGg$iVGPxDdiG5bDVW1yVjfibaiwNSqKHsBOAmvzzfrYd7A"
  },
  errors: [],
  data: #Rockelivery.User<>,
  valid?: true
>
Enter fullscreen mode Exit fullscreen mode

Agora você já pode partir para a parte 2 do projeto :)

Discussion (0)