Phoenix 1.3 is pure love for API development

Phoenix 1.3-rc.0 was just released, and while it’s still not a final release, it already feels solid as h*ck.

I’ve been doing a personal project on the side for a while now, and it has a web API component, which I wrote in Phoenix 1.2. That was my first time using Phoenix on a serious project that I intend to ship.

With the release of Phoenix 1.3, and after I watched Chris McCord’s talk about the changes it brings to the table, I decided to rewrite my API from scratch with this new version.

And oh boy, is it great.

Here are my first-impressions.

New Folder Structure

It has been said a lot of times that Phoenix is "Rails for Elixir." With 1.3, the core team sure wants that notion to be as dead as the web/ folder and the model concept.

I’m not going to explain everything that Chris talked about on his keynote at Lonestar Elixir (you should seriously go watch it), but I think it’s worth saying that the change in the folder structure of a Phoenix project has bigger implications that just files moving from one place to another.

Now, there are concrete boundaries between your actual Elixir application, and the interface that Phoenix provides for it to communicate with the web.

Responsibility separation by design. I love this.

Thinking Ahead

Long gone are the mix phoenix.* commands. Say hi to mix phx.*. Less things to type. Love it.

Also, the generators will make you think in advance of what you want to do before you actually do it.

For instance, in Phoenix 1.2 you could do something like this:

$ mix phoenix.gen.json User users email:string

which then would generate the right migrations, and the model:

defmodule MyApp.User do
    use MyApp.Web, :model
    schema "users" do
        field :email, :string

along with a (web) controller that would handle the CRUD tasks, with the logic for validation within each action. Over time, using this structure, is easy to end up with controller actions over hundred lines long.

Now, with Phoenix 1.3, you need to specify a context for each resource you create (don’t call them "models" anymore 🙄):

$ mix phx.gen.json Accounts User users email:string

In the command above, Accounts is the context module that’s going to be also generated, and is through this context that we’ll interact with our User module:

Accounts.get_user(2) #=> %User{email:}
Accounts.create_user(params) #=> {:ok, new_user}

This is powerful, because now you can define clear boundaries for your application domains.

Say you have the concept of "contacts" in your application. Each user (User) has contacts (also User instances). This can get messy (as I’ve written before). But using this new notion of "contexts", we can have a ContactRelationship context and handle everything in a clean way:

|> ContactRelationships.relationship_with(another_user)
|> ContactRelationships.accept
|> ContactRelationships.get_contacts
|> ContactRelationships.request_contact(another_user)

This scenario would make our folder structure look something like this:

|- lib/
|---- my_app/
|-------- accounts/
|-------- contact_relationships/
|-------- web/

Defining concrete boundaries between responsibilities within your app makes you come up with better code over time.


So far, the additions to the framework are really nice and welcomed. However, I think my favorite one is the new action_fallback plug.

On Phoenix 1.2 and earlier, every controller needed to return a valid conn for every request. Otherwise, an exception would rise.

This is still true, but with the new action_fallback plug we can delegate that task to another controller whose sole purpose is to handle the cases when other controllers couldn’t provide a "successful" response to the request.

Let me explain… before, you wrote code similar to this in every controller action:

def create(conn, %{"user" => user_params}) do
  user = Repo.get_by(User, phone_number: user_params["phone_number"])
  cond do
    user && checkpw(user_params["password"], user.password_hash) ->
      case create_session(user) do
        {:ok, session} ->
            |> put_status(:created)
            |> render("show.json", %{session: session, user: user})
        _ ->
            |> put_status(:unauthorized)
            |> render("error.json")
    true ->
        |> put_status(:unauthorized)
        |> render("error.json")

Is not that bad, truly. But this is still better with Phoenix 1.3:

action_fallback MyApp.Web.FallbackController
def create(conn, %{"user" => user_params}) do
  user = Accounts.get_user_by_phone(user_params["phone_number"])
  cond do
    user && checkpw(user_params["password"], user.hashed_password) ->
      with {:ok, %Session{} = session} <- Sessions.create_session(user) do
        |> put_status(:created)
        |> render("auth_success.json", user: user, session: session)
    true ->
      {:error, :wrong_credentials}

With Phoenix 1.3 our controller actions can be simplified to just care about the "happy path", and leave the rest to the fallback controller that we define using the action_fallback plug.

In this case, when the cond evaluates to true we return from the action {:error, :wrong_credentials}. Since this is not a valid connection, our fallback controller comes forward:

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

By pattern matching, the fallback controller knows exactly what to send back as a response to the request.

This also means that we can have a centralized list of error codes that our API can expose to our clients, on our ErrorView module.

No more error definitions scattered around our codebase. ❤️

However, why stop with errors? If your API has "default" responses for actions, you can rely on the fallback controller too to display them:

def delete(conn, _params) do
  with {:ok, session} <- Authentication.get_session(conn),
       {:ok, deleted} <- Sessions.delete_session(session) do
  else _ ->
    {:error, :unauthorized}

:success will be passed to the appropriate action on your fallback controller, and there goes your default response. 🎉

Final thoughts

Phoenix 1.3 is a great update.

Please keep in mind that the defaults 1.3 offers could be done with 1.2, too. But the fact that this is now the default, really speaks of how much attention to detail the Phoenix team is putting in.

They don’t want us to be using "The Phoenix Way™", but rather provide sensible defaults that enable us to create our own solutions in the best possible way.

Some points:

  • The new fallback_controller plug is awesome.
  • The fact that you need to think in terms of boundaries for your app, I think will eventually lead to better, more maintainable codebases.
  • Smarter code generation is 👌.
  • For APIs, the centralized list of available errors is really welcomed.

I love how productive I am with Phoenix when building APIs, and I’m really looking forward to how this already-great framework will evolve past 1.X.

edit: I also wrote about how action_fallback and contexts in Phoenix 1.3 made my controllers tiny.

Esta entrada fue publicada en Elixir y etiquetada , , , . Enlace permanente.