Elixir API and Elm SPA - Part 1

Part 1: Elixir App creation

I am going to create a demo app with an Elixir API on the backend and a separated Elm SPA on the frontend. The app will be a simple CRUD app with what I have found to be the best practices so far. The app will be a simple market app where users can post announcements for things to sell and other users can see the offers and buy them. The name of the app will be Toltec.

Series

  1. Part 1 - Elixir App creation
  2. Part 2 - Adds Guardian Authentication
  3. Part 3: Elm App creation and Routing setup
  4. Part 4: Adding Login and Register pages
  5. Part 5: Persisting session data to localStorage

Assumptions

I assume you are using:

  • Elixir 1.6.5
  • Erlang 20
  • Elm 0.18
  • PostgreSQL 10.4

Create the App

Let's create a basic app for the API. We don't need html or brunch as we don't render html at all. We'll just expose a JSON REST API:

mix phx.new toltec-api --app toltec --no-brunch --no-html --binary-id

Configure auto formatting

Before adding any code let's configure code auto formatting. Add a file in the root of the project called .formatter.exs:

# .formatter.exs

[
  inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

And the run the format command:

mix format

This should format all the code in your project.

Now you need to configure your editor to automatically run this command when you save a file. For VS Code, for example, you can use the vscode-elixir and vscode-elixir-formatter extensions.

Create Accounts

We are going to use the comeonin package to handle the password hashing in our app. Add this dependency in mix.exs

 # mix.exs

  defp deps do
    [
      {:phoenix, "~> 1.3.0"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_ecto, "~> 3.2"},
      {:postgrex, ">= 0.0.0"},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:comeonin, "~> 4.0"},
      {:argon2_elixir, "~> 1.2"}
    ]
  end

And get the dependencies

mix deps.get

Now let's add the User schema and an Accounts context to hold all this logic

mix phx.gen.context Accounts User users name:string email:string:unique password_hash:string

This will create a user migration, a user schema and an accounts context. For the users I won't use UUID primary keys, so I am going to remove the binary_id config from the users migration, but you're free to keep it if you want. Change the migration to:

# priv/repo/migrations/20180612062911_create_users.exs

  def change do
    execute("CREATE EXTENSION citext;")

    create table(:users) do
      add(:name, :string, null: false)
      add(:email, :citext, null: false)
      add(:password_hash, :string, null: false)

      timestamps()
    end

    create(unique_index(:users, [:email]))
  end

And lets add some basic methods to the user schema:

 # lib/toltec/accounts/user.ex

 defmodule Toltec.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias Toltec.Accounts.User

  schema "users" do
    field(:email, :string)
    field(:name, :string)
    field(:password, :string, virtual: true)
    field(:password_hash, :string)

    timestamps()
  end

  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:name, :email])
    |> validate_required([:name, :email])
    |> validate_length(:name, min: 2, max: 255)
    |> validate_length(:email, min: 5, max: 255)
    |> unique_constraint(:email)
    |> validate_format(:email, ~r/@/)
  end

  def registration_changeset(%User{} = user, attrs) do
    user
    |> changeset(attrs)
    |> cast(attrs, [:password])
    |> validate_required([:password])
    |> validate_length(:password, min: 8, max: 100)
    |> put_password_hash()
  end

  defp put_password_hash(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
        put_change(changeset, :password_hash, Comeonin.Argon2.hashpwsalt(password))

      _ ->
        changeset
    end
  end
end

I have added a password virtual field, to temporarily hold the clear-text password. This will never be saved to the DB. I also added two changesets, one for changing the password and one for the other fields. The put_password_hash() function uses the comeonin library to get a hash of the password and put that on the password_hash field that will be stored in DB.

Now modify the Accounts context that holds all the logic for accounts to look like this:

# lib/toltec/accounts/accounts.ex

defmodule Toltec.Accounts do
  import Ecto.Query, warn: false
  alias Toltec.Repo

  alias Toltec.Accounts.User

  def list_users do
    Repo.all(User)
  end

  def get_user!(id), do: Repo.get!(User, id)

  def create_user(attrs \\ %{}) do
    result =
      %User{}
      |> User.registration_changeset(attrs)
      |> Repo.insert()

    case result do
      {:ok, user} -> {:ok, %User{user | password: nil}}
      _ -> result
    end
  end

  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end

  def delete_user(%User{} = user) do
    Repo.delete(user)
  end

  def change_user(%User{} = user) do
    User.changeset(user, %{})
  end
end

Next, let's add some seed data to the DB to make our app usable.

# priv/repo/seeds.exs

# users
user =
  Toltec.Accounts.User.registration_changeset(%Toltec.Accounts.User{}, %{
    name: "some user",
    email: "user@toltec",
    password: "user@toltec"
  })

Toltec.Repo.insert!(user)

Now is time to build the DB, create tables and insert the seed data.

mix ecto.reset

This command will drop the DB if exists, create it again, run the migrations in order and finally run the seeds.exs file to insert initial data. You should see no errors in the output, and should be similar to this.

mix ecto.reset
The database for Toltec.Repo has already been dropped
The database for Toltec.Repo has been created
[info] == Running Toltec.Repo.Migrations.CreateUsers.change/0 forward
[info] execute "CREATE EXTENSION citext;"
[info] create table users
[info] create index users_email_index
[info] == Migrated in 0.0s
[debug] QUERY OK db=2.8ms
INSERT INTO "users" ("email","name","password_hash","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" ["user@toltec", "some user", "$argon2i$v=19$m=65536,t=6,p=1$jdZHJ4HdGSF34hP6iTiPeQ$+PWBp77RP8wxMasVj0wv1wWS33pponixKSEG109V/u8", {{2018, 6, 12}, {22, 35, 55, 411334}}, {{2018, 6, 12}, {22, 35, 55, 411345}}]

You can find the source code, with tests, in the repo here in the branch part-01.

After cloning it, run the tests and verify that everything is alright:

mix test
...................

Finished in 0.3 seconds
19 tests, 0 failures

So far we have created the app, the users schema, and some initial user to play with our app. That's it for now.