In part 1, we created our index action for our todo list in Phoenix.

Today we will continue with the create action.

Create a todo item

Let's start by implementing the create action of the todo list.

As always we start with a test.

Open todo_controller_test.exs.

 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
   
    # New 
    describe "POST /todos" do
      test "with valid params" do
        params = %{title: "a new todo"}
        conn = post conn(), "/api/todos", %{todo: params}
        response = Poison.Parser.parse!(conn.resp_body)
        assert conn.status == 201
        assert response["title"] == params.title
      end
    end

When we run our test

mix test test/controllers

  1) test POST /todos with valid params (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:14
     Assertion with == failed
     code: conn.status() == 201
     lhs:  404
     rhs:  201
     stacktrace:
      test/controllers/todo_controller_test.exs:18: (test)

The test indicates that we miss our create route.

Let's edit router.ex and add it.

  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
        post "/todos", TodoController, :create # New
      end
    end
  end

mix test test/controllers

1) test POST /todos with valid params (TodoListPhoenixApi.TodoControllerTest)
   test/controllers/todo_controller_test.exs:14
   ** (UndefinedFunctionError) function TodoListPhoenixApi.TodoController.create/2 is undefined or private

It is time to add our create action.

In 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
   
    # New 
    def create(conn, %{"todo" => todo_params}) do
      todo = Todo.changeset(%Todo{}, todo_params)
      case Repo.insert(todo) do
        {:ok, todo} -> render conn |> put_status(201), todo: todo
      end
    end
  end
  1) test POST /todos with valid params (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:14
     ** (Phoenix.Template.UndefinedError) Could not render "create.json" for TodoListPhoenixApi.TodoView, please define a matching clause for render/2 or define a template at "web/templates/todo". No templates were compiled for this module.
     Assigns:

As the test indicates we miss the create method in our view.

In web/views/todo_view.ex.

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

mix test test/controllers

  Finished in 0.1 seconds
  2 tests, 0 failures

Nice!

Let's add one more test in our describe block, this time to assert for validation errors.

 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

   describe "POST /todos" do
     test "with valid params" do
       params = %{title: "a new todo"}
       conn = post conn(), "/api/todos", %{todo: params}
       response = Poison.Parser.parse!(conn.resp_body)
       assert conn.status == 201
       assert response["title"] == params.title
     end
    
     # New
     test "with invalid params" do
        params = %{title: ""}
        conn = post conn(), "/api/todos", %{todo: params}
        response = Poison.Parser.parse!(conn.resp_body)
        errors = Enum.at(response["errors"], 0)
        assert conn.status == 422
        assert errors["title"] == "can't be blank"
     end
  end 
end

mix test test/controllers

  1) test POST /todos with invalid params (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:22
     ** (CaseClauseError) no case clause matching: {:error, #Ecto.Changeset<action: :insert, changes: %{}, errors: [title: {"can't be blank", []}], data: #TodoListPhoenixApi.Todo<>, valid?: false>}

Time to get a taste of Elixir pattern matching.

Back to our controller.

  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
 
    def create(conn, %{"todo" => todo_params) do
      todo = Todo.changeset(%Todo{}, todo_params)
      case Repo.insert(todo) do
        {:ok, todo} -> render conn |> put_status(201), todo: todo
        {:error, changeset} -> render(conn |> put_status(422), "error.json", changeset: changeset) # New
      end
    end
  end

mix test test/controllers

  1) test POST /todos with invalid params (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:22
     ** (Phoenix.Template.UndefinedError) Could not render "error.json" for TodoListPhoenixApi.TodoView, please define a matching clause for render/2 or define a template at "web/templates/todo". No templates were compiled for this module.
     Assigns:

And back to our Todo view.

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

    # New
    def render("error.json", %{changeset: changeset}) do
      errors = Enum.map(changeset.errors, fn {attr, error} ->
        %{"#{attr}": format_error(error)}
      end)
      %{errors: errors}
    end

    # New
    defp format_error({message, values}) do
      Enum.reduce values, message, fn {k, v}, acc ->
        String.replace(acc, "%{#{k}}", to_string(v))
      end
    end
  end

Let's pause for a sec and see what's going on here.

Errors from Ecto come as a keyword list.

They are in the form of:

  [title: {"can't be blank", []}]

Unfortunately, this is not what Poison expects in order to parse them into json.

So in our #render method, we are mapping the keyword list into a map.

But wait.

If in our case "can't be blank" is the error message then, what is that empty list in the tuple?

This list, is being used to store any possible interpolated values that would go into the error message itself.

An example would be:

  [title: {"should be at least %{count} characters", [count: 3]i}]

Therefore, we define format_error, so as to replace the interpolated variables to its actual values.

I know, it's not the most convenient way to render errors but José Valim explains the reasoning behind it thanks to kurtisnelson pull request.

Let's run our test and see it passing this time!

mix test test/controllers

  Finished in 0.2 seconds
  3 tests, 0 failures   

Cool.

That's it for today.

Part 3 it's on the way.

Till next time.