Welcome back!

Previously, we implemented the index action in part 1 as well as the create action in part 2.

Today, we are going to finish our API implementation.

Update a todo item

Open test/controllers/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
   
    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
    
      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

    # New
    describe "PUT /todos/:id" do
      test "with valid params" do
        todo = insert(:todo)
        params = %{title: "my title changed"}
        conn = put conn, "/api/todos/#{todo.id}", %{todo: params}
        response = Poison.Parser.parse!(conn.resp_body)
        assert conn.status == 200
        assert response["title"] == params.title
      end
    end
  end

mix test test/controllers

 1) test #PUT /todos/:id with valid params (TodoListPhoenixApi.TodoControllerTest)
    test/controllers/todo_controller_test.exs:33
    Assertion with == failed
    code: conn.status() == 200
    lhs:  404
    rhs:  200

Let's add the route to our 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
      post "/todos", TodoController, :create 
      put "/todos/:id", TodoController, :update # New
    end
  end

mix test test/controllers

1) test #PUT /todos/:id with valid params (TodoListPhoenixApi.TodoControllerTest)
   test/controllers/todo_controller_test.exs:33
   ** (UndefinedFunctionError) function TodoListPhoenixApi.TodoController.update/2 is undefined or private

Inside our 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

    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)
      end
    end

    # New   
    def update(conn, %{"id" => id, "todo" => todo_params}) do
      todo = Repo.get!(Todo, id)
      changeset = Todo.changeset(todo, todo_params)
      case Repo.update(changeset) do
        {:ok, todo} -> render conn |> put_status(200), todo: todo
      end
    end
  end

mix test test/controllers

  1) test PUT /todos/:id with valid params (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:33
     ** (Phoenix.Template.UndefinedError) Could not render "update.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.

In todo_view.ex

  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("update.json", %{todo: todo}) do
      todo
    end
 
    def render("error.json", %{changeset: changeset}) do
      errors = Enum.map(changeset.errors, fn {attr, error} ->
        %{"#{attr}": format_error(error)}
      end)
      %{errors: errors}
    end
 
    defp format_error({message, values}) do
      Enum.reduce values, message, fn {k, v}, acc ->
        String.replace(acc, "%{#{k}}", to_string(v))
      end
    end
  end

mix test test/controllers

  Finished in 0.2 seconds
  4 tests, 0 failures

Like we did with the create action we should evaluate whether the request was valid or not.

Back to our test/controllers/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
   
    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
    
      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

    describe "PUT /todos/:id" do
      test "with valid params" do
        todo = insert(:todo)
        params = %{title: "my title changed"}
        conn = put conn(), "/api/todos/#{todo.id}", %{todo: params}
        response = Poison.Parser.parse!(conn.resp_body)
        assert conn.status == 200
        assert response["title"] == params.title
      end
      
      # New
      test "with invalid params" do
        todo = insert(:todo)
        params = %{title: ""}
        conn = put conn(), "/api/todos/#{todo.id}", %{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 PUT /api/todos/:id with invalid params (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:43
     Assertion with == failed
     code: conn.status() == 422
     lhs:  404
     rhs:  422

In 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

    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)
      end
    end

    def update(conn, %{"id" => id, "todo" => todo_params}) do
      todo = Repo.get!(Todo, id)
      changeset = Todo.changeset(todo, todo_params)
      case Repo.update(changeset) do
        {:ok, todo} -> render conn |> put_status(200), todo: todo
        {:error, changeset} -> render(conn |> put_status(422), "error.json", changeset: changeset) # New   
      end
    end
  end

mix test test/controllers

  Finished in 0.2 seconds
  5 tests, 0 failures

Destroy a todo item

In our 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
   
    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
    
      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

    describe "PUT /todos/:id" do
      test "with valid params" do
        todo = insert(:todo)
        params = %{title: "my title changed"}
        conn = put conn(), "/api/todos/#{todo.id}", %{todo: params}
        response = Poison.Parser.parse!(conn.resp_body)
        assert conn.status == 200
        assert response["title"] == params.title
      end
      
      test "with invalid params" do
        todo = insert(:todo)
        params = %{title: ""}
        conn = put conn(), "/api/todos/#{todo.id}", %{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

    test "DELETE /todos/:id" do
      todo = insert(:todo)
      conn = delete conn(), "/api/todos/#{todo.id}"
      assert conn.status == 204
    end
  end

mix test test/controllers

  1) test DELETE /api/todos/:id (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:54
     Assertion with == failed
     code: conn.status() == 204
     lhs:  404
     rhs:  204

In 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
      post "/todos", TodoController, :create 
      put "/todos/:id", TodoController, :update
      delete "/todos/:id", TodoController, :destroy # New
    end
  end

mix test test/controllers

  1) test DELETE /todos/:id (TodoListPhoenixApi.TodoControllerTest)
     test/controllers/todo_controller_test.exs:54
     ** (UndefinedFunctionError) function TodoListPhoenixApi.TodoController.destroy/2 is undefined or private

Let's add the destroyaction.

  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)
      end
    end

    def update(conn, %{"id" => id, "todo" => todo_params}) do
      todo = Repo.get!(Todo, id)
      changeset = Todo.changeset(todo, todo_params)
      case Repo.update(changeset) do
        {:ok, todo} -> render conn |> put_status(200), todo: todo
      end
    end
    
    # New   
    def destroy(conn, %{"id" => id}) do
      todo = Repo.get!(Todo, id)
      Repo.delete!(todo) 
      conn
      |> send_resp(204, "")
    end
  end

mix test test/controllers

  Finished in 0.2 seconds
  6 tests, 0 failures

We just finished our Phoenix API!

What? Where are the show, new and edit actions?

Apparently we will not need them.

The reason?

Well, for this tutorial we are going to use React Native.

And?

It's going to handle all of these actions in its UI.

As always you can find the source code of the API here.

See you soon!