Yesterday I posted about how Phoenix 1.3 was pure love for API development. In that post, I mentioned that one of my favorites features in this new release is the action_fallback plug.

Today, I want to talk a little bit more about a killer combo I’ve uncovered: action_fallback + contexts.

I’m still amazed at how Phoenix 1.3 enables my controllers to be so tiny.



# Background

In my application, I have a `User` module that’s managed through an `Accounts` context:

def update(conn, %{"id" => id, "user" => user_params}) do user = Accounts.get_user(id) with {:ok, %User{} = user} <- Accounts.update_user(user, user_params), do: render(conn, "show.json", user: user) end

The default MyApp.Web.FallbackController that’s generated when you first create your app includes, amongst others, a call/2 definition that accepts an %Ecto.Changeset{} instance:

defmodule MyApp.Web.FallbackController do use MyApp.Web, :controller def call(conn, {:error, %Ecto.Changeset{} = changeset}) do conn |> put_status(:unprocessable_entity) |> render(MyApp.Web.ChangesetView, "error.json", changeset: changeset) end # end

This function will be called if, for instance, the update/2 method on UserController failed to pattern match the with statement. Repo.update/1 returns {:error, changeset} in case it fails. Since I didn’t define an else path, {:error, changeset} would be then forwarded to MyApp.Web.FallbackController .



# Custom Errors

I like to define custom errors for the domains for my app. The default behavior on `MyApp.Web.ChangesetView` will output something like this:

{ errors: [ "username has already been taken" ] }

I modified MyApp.Web.ChangesetView to expose the error the way I wanted:

defmodule MyApp.Web.ChangesetView do use MyApp.Web, :view alias MyApp.ResponseWrapper def translate_errors(changeset) do case hd(changeset.errors) do {label, {"has already been taken", _}} -> "#{label}_taken" {:password, {"should be at most %{count} character(s)", _}} -> "password_too_long" {:password, {"should be at least %{count} character(s)", _}} -> "password_too_short" _ -> :something_went_wrong end end def render("error.json", %{changeset: changeset}) do ResponseWrapper.error translate_errors(changeset) end end

In the translate_errors/1 function, I take the first error off the changeset, and then pattern match its content. I can add more patterns to match later depending on my needs. Now, MyApp.Web.ChangesetView outputs errors like this:

{ status: "error", error: "username_taken" } { status: "error", error: "password_too_short" }



# Benefits

What’s great about that, is that now I can basically reuse that same code for any other module that needs to be inserted in my database.

This lets me write code like this:

defmodule MyApp.Web.DeviceController do use MyApp.Web, :controller alias MyApp.{Authentication, Notifications} plug :scrub_params, "device" when action in [:create] plug Authentication action_fallback MyApp.Web.FallbackController def create(conn, %{"device" => device_params}) do with {:ok, user} <- Authentication.get_current_user(conn), {:ok, _dev} <- Notifications.add_device(device_params, user), do: :success end end

My create/2 function in DeviceController is just 3 lines long. In those 3 lines, I:

Get the current user. Insert a new device (with changeset validations) to the database. Passes :success to my the action_fallback plug.

The “happy path” response from my fallback looks like this:

{ status: "success", data: { status: "success" } }

If any of the guards in the with statement fails, guess what: my fallback controller’s got my back 😬.

For instance, say the Notifications.add_device/2 function fails to insert the new device into the database because it didn’t meet one of the changeset validations, such as the unique_constraint for device_token .

Notifications.add_device would then return {:error, changeset} , which would be forwarded to my fallback controller, and the JSON error would look like this:

{ status: "error", error: "device_token_taken" }

And I got that for free. 👆