by

Working with the new Phoenix 1.3 directory structure – A Love Story

Recently I had an opportunity to build a project with the not-yet-released Phoenix 1.3. This minor version bump includes some optional new features that, for me, greatly improved the ergonomics of developing my project. I have no insider info into the project or the motivations behind these changes, but I can say as someone that has worked with Phoenix in fits and starts since its pre-1.0 days that on the whole I really enjoyed them.

Off the top of my head while its fresh here are my thoughts on it. This won’t be an exhaustive list of changes because I’m lazy and on vacation. So I’m just going to pull the most notable features from memory, which I think has its own sort of value in that these were the most memorable to someone focused primarily on developing the Elixir side of an app.

In Phoenix 1.3 the /web folder has been moved inside /lib/<project>/web to be more in line with a typical Mix application. To anyone used to a Phoenix project this would be the most immediately noticeable change. Along with this change, all of your controllers and views will also be namespaced inside of Web. For example, the standard Project.PageController that comes with the generator becomes Project.Web.PageController, and the Project.PageView becomes Project.Web.PageView

My first impression of this change is that Phoenix is trying to become more in line with traditional Elixir/Erlang OTP app structures, including their Supervision tree structure and I support that 100%.

In the keynote where these changes were announced, they talked about visualizing your Phoenix code as just one way to interact with your underlying application, which could have many other ways. This is already true even within Phoenix. If your application has an API and a UI, then you most likely have multiple avenues of achieving the same result. Bringing this out explicitly is a huge win for developers.

One of the ways that Phoenix 1.3 makes this explicit is also the next big change in the app. Now, when you generate a new resources (whether through gen.html or gen.shema [the gen.model replacement]), you also have to specify a Context. From the docs:

The context is an Elixir module that serves as an API boundary for the given resource. A context often holds many related resources. Therefore, if the context already exists, it will be augmented with functions for the given resource. Note a resource may also be split over distinct contexts (such as Accounts.User and Payments.User).

To me, this is where 1.3 really shines. When I was first wrapping my head around it, I tended to use Domain a lot in my head instead of Context, which was helpful to me. When you generate a new resource, the context is also generated for you, along with an outline of the functions that probably belong there.


defmodule Project.Accounts do
  @moduledoc """
  The boundary for the Accounts system.
  """

  import Ecto.{Query, Changeset}, warn: false
  alias Project.Repo

  alias Project.Accounts.User

  @doc """
  Returns the list of users.

  ## Examples

      iex> list_users()
      [%User{}, ...]

  """
  def list_users do
    Repo.all(User)
  end

  @doc """
  Gets a single user.

  Raises `Ecto.NoResultsError` if the User does not exist.

  ## Examples

      iex> get_user!(123)
      %User{}

      iex> get_user!(456)
      ** (Ecto.NoResultsError)

  """
  def get_user!(id), do: Repo.get!(User, id)

  ...
end

This lends itself perfectly to building an application around multiple access points to your data. It’s also something that I haven’t seen in any other frameworks I’ve worked with. This sort of organization is typically left as an exercise for the user.

Here is how the Contexts ended up influencing my app design.

I was building an app that had multiple “accounts” that it needed to track, so I had an Accounts.User, I had a Github.User, and I had a Slack.User, each responsible for storing its own data. Inside each of those contexts were the functions I needed to work with the resources it contained.

For example, I needed to be able log in and register as an Accounts.User with Guardian, so these functions got added to the context:


  def authenticate(params) do
    find_from_auth(params)
      |> validate_password(params["password"])
  end

  def register(%{"password" => pass, "password_confirmation" => conf} = params) when pass == conf do
    do_registration(params)
  end
  def register(params) do
    {:error, "Passwords do not match"}
  end

In my Slack.User, I needed ways to associate it to an Accounts.User and so I had helper functions over there as well. I had function in Github.User for maintaining the link between my user and their accounts api. I also built a had a Settings context for a user, and the Settings context knew how to load the settings applicable to the whatever model was provied. I wanted Slack.Users  settings to be aware of Slack Channels and teams as well as just the user, and the context provided a good place to house these separate semantics.

For me, context’s a very welcome abstraction. In my previous Phoenix project it was always a bit confusing as to whether something belonged in /web or in /lib.  That project grew to be pretty hefty,  and ended up having a lib/data_store/ folder which was vaguely similar to what Contexts provide. What I was reaching for was a place to hold the code that in an OO framework like Rails would be shoved on to the model. I love the Repo pattern that Phoenix uses but I did not love includeing Ecto.Query everywhere that I needed to lookup a record. Contexts provide a clear place for holding that code in an Elixir way.

Taken together, I think contexts and the move of web into /lib/project is a clear win. It leads to a more well organized project and in the end I think it will save many headaches. I think it is a project structure that provides clear avenues for growth. Having that structure by default, rather than solving for a simpler use case, really sets Phoenix apart.

I’m very excited to keep building stuff with Elixir and Phoenix. From an outsiders perspective the team has really taken on the hard challenges head on and really moved forward with them.

That new project I build is a bridge to work with GitHub Issues inside of Slack, you should check it out.

💘😽🎉