Phoenix is a framework that was inspired by Rails and comes to bridge the gap between developer's productivity and application performance.

It is based in Elixir, a relative new programming language that takes advantage of Erlang VM performance, with a syntax similar to Ruby.

Coming from a Rails background I found a lot of similarities with Phoenix which we are going to explore further on.

Today, we are going to find out how we can create a todo list api.

Enough with the talk, let's fasten our seatbelts and get started!

Preface

For this tutorial I assume that you are already familiar with Elixir.

You can find Phoenix installation instructions here.

Create a new phoenix project

Tip

You can get the list of all options in the creation of a new project with mix help phoenix.new.

Fire up your terminal and execute mix phoenix.new todo_list_phoenix_api --no-brunch --no-html.

  Fetch and install dependencies? [Yn]

We type y and press ENTER to continue.

After the initialisation of the original project files, we move into our newly created folder.

cd todo_list_phoenix_api

And we create our project's database mix ecto.create.

Most likely you will receive errors about the postgres role that your db is missing.

Don't worry we will resolve this issue.

Use the command line tool psql and execute the following:

  1. CREATE ROLE postgres;
  2. ALTER ROLE postgres CREATEDB;
  3. ALTER ROLE postgres LOGIN;

Great, now type \q to exit the console.

Run for the last time mix ecto.create

  The database for TodoListPhoenixApi.Repo has been created

Yay!

This time you should get no errors!

Display the todo list

For the whole process of developing our app, we are going to use a full TDD approach.

Elixir provides us with its own test library Exunit.

We will also going to install Ex_machina a powerful library to create our test data.

Edit your mix.exs and make sure it looks like this.

  def application do
    [mod: {TodoListPhoenixApi, []},
     applications: [:phoenix, :phoenix_pubsub, :cowboy, :logger, :gettext,
                    :phoenix_ecto, :postgrex, :ex_machina]] # New
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
  defp elixirc_paths(_),     do: ["lib", "web"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [{:phoenix, "~> 1.2.1"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_ecto, "~> 3.0"},
     {:postgrex, ">= 0.0.0"},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"},
     {:ex_machina, "~> 1.0"}] # New
  end

After you finish with the installation instructions type mix deps.get to install all the dependencies.

Let's test that our test database works properly.

Start by executing mix test.

The first time you run the tests it takes some time, as phoenix compiles our application files with the .ex extension.

Grab yourself a cup of tea.

  ...

  Finished in 0.05 seconds (0.05s on load, 0.00s on tests)
  3 tests, 0 failures

  Randomized with seed 903779 

Our app will have a todos controller so let's create our test file.

touch test/controllers/todo_controller_test.exs

Let's open our new test file start writing our test.

  defmodule TodoListPhoenixApi.TodoControllerTest do
    use TodoListPhoenixApi.ConnCase, async: true
  
    import TodoListPhoenixApi.Factory

    test "GET /" do
      todos = insert_list(3, :todo)
      conn = get conn(), "/api/todos"
      assert conn.status == 200
      assert conn.resp_body == Poison.encode!(todos)
    end
  end

And run it mix test.

** (CompileError) test/controllers/todo_controller_test.exs:4: 
module TodoListPhoenixApi.Factory is not loaded and could not be found

The Force eh.. the test guides us to create our Factory

Let's create the file.

touch lib/todo_list_phoenix_api/factory.ex

And define it:

  defmodule TodoListPhoenixApi.Factory do
    use ExMachina.Ecto, repo: TodoListPhoenixApi.Repo

    def todo_factory do
      %TodoListPhoenixApi.Todo{
        title: "I have something to do"
      }
    end
  end

Then we run our test.

mix test

== Compilation error on file lib/todo_list_phoenix_api/factory.ex ==
** (CompileError) lib/todo_list_phoenix_api/factory.ex:5: TodoListPhoenixApi.Todo.__struct__/0 is undefined, 
cannot expand struct TodoListPhoenixApi.Todo(elixir) src/elixir_map.erl:58: :elixir_map.translate_struct/4

Our Model is missing let's generate it.

mix phoenix.gen.model Todo todos title:string

And run our tests again.

mix test

    1) test GET / (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:6
     Assertion with == failed
     code: conn.status() == 200
     lhs:  404
     rhs:  200
     stacktrace:
       test/controllers/todo_controller_test.exs:9

..

Finished in 0.2 seconds (0.1s on load, 0.1s on tests)
6 tests, 1 failure

Let's add our route to router.ex.

  defmodule TodoListPhoenixApi.Router do
    use TodoListPhoenixApi.Web, :router
    
    pipeline :api do
      plug :accepts, ["json"]
    end

    scope "/api", TodoListPhoenixApi do
      pipe_through :api
      get "/todos", TodoController, :index # New
    end
  end

mix test

1) test GET / (TodoListPhoenixApi.TodoControllerTest)
  test/controllers/todo_controller_test.exs:6
  ** (UndefinedFunctionError) undefined function TodoListPhoenixApi.TodoController.init/1 (module TodoListPhoenixApi.TodoController is not available)

WoW test didn't pass

Ok at least we have a failing test, thats something

The error implies that we do not have a controller yet

touch web/controllers/todo_controller.ex

  defmodule TodoListPhoenixApi.TodoController do
    use TodoListPhoenixApi.Web, :controller
  
    alias TodoListPhoenixApi.Todo
  
    def index(conn, _params) do
      todos = Repo.all(Todo)
      render conn, todos: todos
    end
  end

mix test test/controllers

1) test GET / (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:6
     ** (UndefinedFunctionError) undefined function TodoListPhoenixApi.TodoView.render/2 (module TodoListPhoenixApi.TodoView is not available)

The error indicates that we have no view.

It's worth mentioning that, unlike Rails views, views in Phoenix act like decorators as it is explained in the documentation.

Thus, Phoenix views are being used so as to expose our models business logic to templates or to json like in our API.

touch web/views/todo_view.ex

  defmodule TodoListPhoenixApi.TodoView do
    use TodoListPhoenixApi.Web, :view
  
    def render("index.json", %{todos: todos}) do
      todos
    end
  end

And let's run our test again.

mix test test/controllers

  1) test GET / (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:6
     ** (Poison.EncodeError) unable to encode value: {nil, "todos"}

A new error... At least we are progressing.

We want our model to support json.

And Poison comes into play.

 defmodule TodoListPhoenixApi.Todo do
   use TodoListPhoenixApi.Web, :model
   @derive {Poison.Encoder, only: [:title, :id]}

   schema "todos" do
     field :title, :string

     timestamps()
   end

   @doc """
   Builds a changeset based on the `struct` and `params`.
   """
   def changeset(struct, params \\ %{}) do
     struct
     |> cast(params, [:title])
     |> validate_required([:title])
   end
 end

mix test test/controllers

  Finished in 0.1 seconds (0.08s on load, 0.1s on tests)
  1 test, 0 failures

We finally finished part 1 of Phoenix API with React Native tutorial!

You can find the code up to this point here.

Stay tuned for part 2.