Phoenix framework is a web framework for Elixir programming language, which became the go-to set of libraries when we start new project for our clients. It’s performant and robust, flexible and has sane defaults - we enjoy working with Phoenix a lot.
However nice and programmer-friendly the framework may be, there is always other pieces that build your application, and most important of these is your own code.
Code refactoring is a critical step during development of applications here at AmberBit. Whenever we have the pull request ready to be submitted and reviewed by other developers, we usually take a last look at the code we wrote and decide what to refactor and how. Everyone likes pretty code!
One of the most usual suspects that get picked as a part that needs refactoring are Phoenix controllers. Let’s have a look at several techniques we use to make our code look pretty.
Technique #1: Extract logic to Plugs
Plug is a library that Phoenix build on top of, and it provides basic interface to deal with HTTP requests and web servers in Elixir applications. Good place to start learning about Plug is this section of ElixirSchool.
Whenever you have code which repeats in multiple controller actions or simply has to be executed before each action - creating a custom plug that does the required job is the way to go.
Let’s have a look at the following controller:
defmodule MyApp.Web.PostsController do
  use MyApp.Web, :controller
  def index(conn, _params) do
    conn = conn
           |> assign(:current_user, find_current_user(conn))
    case conn.assigns.current_user do
      nil ->
        conn
        |> put_flash(:info, "Please sign in!")
        |> redirect(to: login_path(conn, :new))
        |> halt()
      _user ->
        conn
        |> render("index.html")
    end
  end
  ...
  defp find_current_user(conn) do
    ...
  end
endWe want to make sure we show the page only to signed in users. In all other cases we want to redirect to login page.
It’s fine to have such code in one place of application, maybe two - but
if you have to call find_current_user/1 and conditionally branch off
your program flow with case or another conditional syntax construct in
every single controller and every single action (except login page) this
quickly becomes tiresome.
The way I like to break it down is to introduce not one - but two custom plugs: one which finds currently signed in user, and another one that redirects user to login page if user is not signed in. This give us flexibility to have pages where being signed in is not required - but it can enable personalized / additional functionality - such as adding comments, and have other pages that require user to be signed in.
defmodule MyApp.Plugs.FindCurrentUser do
  import Plug.Conn
  def init(_opts) do
    :ok
  end
  def call(conn, _) do
    conn
    |> assign(:current_user, find_current_user(conn))
  end
  defp find_current_user(conn) do
    ...
  end
end
defmodule MyApp.Plugs.RequireCurrentUser do
  import Plug.Conn
  def init(_opts) do
    :ok
  end
  def call(conn, _) do
    case conn.assigns[:current_user] do
      nil ->
        conn |> redirect(to: login_path(conn, :new)) |> halt()
      _user ->
        conn
    end
  end
endThere are (at least) two ways to use our brand new plugs in controllers:
- Directly instructing Phoenix to execute them - form within controllers themselves.
 - By adding these plugs to a pipeline in router
 
Let’s see how our controller looks like if we go with option 1:
defmodule MyApp.Web.PostsController do
  use MyApp.Web, :controller
  plug MyApp.Plugs.FindCurrentUser
  plug MyApp.Plugs.RequireCurrentUser
  def index(conn, _params) do
    conn
    |> render("index.html")
  end
endBetter? Better.
In case you want to exclude plugs executing from certain controller actions - or selectively specify before which actions plug should be executed, you can add guard clauses directly in your controllers too.
Usually more handy and less error-prone way than adding plugs to your stack in controllers is to alter your router to do so for you:
defmodule MyApp.Router do
  use MyApp.Web, :router
  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug MyApp.Plugs.FindCurrentUser # add this line
  end
  pipeline :require_current_user do
    plug MyApp.Plugs.RequireCurrentUser
  end
  ...
  scope "/", MyApp do
    pipe_through [:browser]
    get "/login", LoginController, :new
    ...
  end
  scope "/", MyApp do
    pipe_through :browser
    pipe_through :require_current_user
    resources "/posts", PostsController
    ...
  end
  ...
end
As you can see above, you can alter your standard :browser pipeline to
include plug that finds and assigns current user to
conn.assigns.current_user, and create additional pipeline which
ensures user is signed in. All the routes defined in the second scope
block will redirect to login page if user is not signed in.
This way you can easily separate your public pages from these that require being signed in to account in your system.
The same technique can be applied to several other cases, such as:
- 
finding and assigning parent resources in case of nested resources
such as 
/posts/:post_id/comments/:comment_id - introducing authorization / permission checking based on controller/action/role given user has in the system
 - setting language / locales based on cookie
 - finding current tenant in multi-tenant system based on domain/subdomain
 
and many other similar use cases.
Technique #2: Extract logic to workflows (a.k.a. services)
Whenever your controllers become bloated, and you have multiple helper functions in them - good idea may be to move the business logic to separate modules. Doing so helps with testability and code re-use, but most importantly leaves the controllers to do one thing and one thing only: control how and what to render, depending on the outcome of some business logic process.
Personally I think best course of action, for most controllers and actions, is to delegate as much as you can, possibly each action - to it’s own module / function that defines it’s behavior.
Let’s consider the following controller:
defmodule MyApp.Web.RegistrationController do
  use MyApp.Web, :controller
  plug MyApp.Plugs.RequireNoUser # custom plug we learned above about
  plug :put_layout, "no_user.html"
  def new(conn, _params) do
    conn
    |> render("new.html", changeset: MyApp.User.changeset(%MyApp.User{}, %{}))
  end
  def create(conn, %{"registration" => registration_params}) do
    user_changeset = MyApp.User.changeset(%MyApp.User{}, registration_params)
    case MyApp.Repo.insert(user_changeset) do
      {:error, changeset} ->
        conn
        |> render("new.html", changeset: changeset))
      {:ok, user} ->
         user
         |> seed_example_dashboard_data()
         |> send_email_confirmation_request()
         |> send_admins_new_user_notirication()
         conn
         |> sign_in_user(user)
         |> redirect(to: dashboard_path(@conn, :show))
    end
  end
  defp send_email_confirmation_request(user) do
     ...
  defp send_admins_new_user_notification(user) do
     ...
  defp sign_in_user(user) do
     ...
  defp seed_example_dashboard_data(user) do
     ...
endIt’s a pretty standard signup controller, handling new user registration and onboarding. While it’s pretty standard - there is still plenty of things going on. Not only we validate the form (here using directly changeset and Ecto schema backed by database), but after user registered we have to seed their dashboard with example data, send them confirmation e-mail with a link, send notification to admins and sign user to the system. This could easily mean this single controller has already 200 lines of code or more. And that’s just a single action.
How I like to deal with bloated controllers is moving the business logic to reusable services. I have noticed these services often take a form of multi-step algorithms, and for a while I adopted naming convention of calling them “workflows”. I must admit, I stole the naming from another Elixir developer, but I think I also infected others with the idea since, which suggests it’s not terribly bad.
Let’s see how can we extract the business logic to a workflow:
defmodule MyApp.Registration.Workflow do
  def run(registration_params) do
    with user_changeset <- MyApp.User.changeset(%MyApp.User{}, registration_params),
         {:ok, user} <- MyApp.Repo.insert(user_changeset),
         :ok <- seed_example_dashboard_data(user),
         :ok <- send_email_confirmation_request(user),
         :ok <- send_admins_new_user_notirication(user) do
           generate_sign_in_token(user)
    end
  end
  defp send_email_confirmation_request(user) do
     ...
  defp send_admins_new_user_notification(user) do
     ...
  defp generate_sign_in_token(user) do
     ...
  defp seed_example_dashboard_data(user) do
     ...
endAnd we can use it in controller like this:
defmodule MyApp.Web.RegistrationController do
  use MyApp.Web, :controller
  plug MyApp.Plugs.RequireNoUser # custom plug we learned above about
  plug :put_layout, "no_user.html"
  def new(conn, _params) do
    conn
    |> render("new.html", changeset: MyApp.User.changeset(%MyApp.User{}, %{}))
  end
  def create(conn, %{"registration" => registration_params}) do
    case MyApp.Registration.Workflow.run(registration_params) do
      {:error, changeset} ->
        conn
        |> render("new.html", changeset: changeset))
      {:ok, sign_in_token} ->
         conn
         |> sign_in_user_with_token(sign_in_token)
         |> redirect(to: dashboard_path(@conn, :show))
    end
  end
  defp sign_in_user_with_token(token) do
    ...
endBetter? I think so. Our controller is not concerned with generating token, sending e-mails nor dealing with database - it delegates all the hard work to dedicated workflow, while being focused on being a bridge between framework and your business logic. It’s almost like controllers are piece of infrastructure, and you can stop thinking about them as part of your “core” applications. Workflows/services are where the logic lives.
The bonus part about extracting business logic to workflows is that they are testable independently from controllers and Phoenix itself. You can test them in isolation.
Writing tests for workflows/services is usually easier and quicker to writing full controller tests or writing tests with something like Selenium/Wallaby combo.
What I like to do on the testing side of things is to write “happy path” tests using Wallaby, and leave edge cases to be tested with unit tests testing workflow itself. In the case of our example registration workflow and controller, I would write these Wallaby tests:
defmodule MyApp.Integration.RegistrationTest do
  ...
  test "should register and sign in user if they filled in form correctly"
  test "should display validation errors and not register user if the form is incorrect"
endand then add fasts tests that do not rely on real browser to test some edge cases:
defmodule MyApp.Registration.WorkflowTest do
  ...
  test "should insert new user to database after successfull registration"
  test "should generate and send e-mails to user and admins"
  test "should generate sign in token to be stored in the cookie"
  test "should require e-mail, first and last name"
  test "should require e-mail in valid format"
  test "should make sure e-mail is unique in the system"
endTechnique #3: Using tableless Ecto schemas for custom form validations
In the examples above we have extracted some business logic to custom
plugs, and then extracted more business logic to workflows. Our
registration workflow is now dealing with database directly to perform
Repo.insert(changeset), but we still have to initialize the form with:
...
  def new(conn, _params) do
    conn
    |> render("new.html", changeset: MyApp.User.changeset(%MyApp.User{}, %{}))
  end
...This is not ideal for two reasons:
Our controllers need to know about our Ecto schemas. You can argue that’s not a big issue, and I’ll agree, but it’s always nice to have separation of layers in your application where upper layer does not necessarily reach the bottom layer, but goes through the intermediary. In our case we have introduced workflow/services layer that we use
createaction, but not innew.We are cluttering our Ecto schemas with business logic. This is a bigger problem, because it can possibly lead to security issues, where we unwillingly expose some database fields to be public where in fact we want them to be protected from user writes.
Let’s focus on reason 2 for a while. If our User model has is_admin
field, which determines their system-wide administrative rights, and our
User.changeset/2 function allows setting this field, we have a
possible security vulnerability in our registration form. Even though
the form we prepared has no field for is_admin in it, it doesn’t mean
a hacker can’t insert a field in the form. This can be as simple as
right-clicking in Chrome on the page, going to Developer Tools and
writing a single line of code to our HTML form:
<input type="hidden" name="user[is_admin]" value="true" />User can register and obtain illegitimately admin rights. Oops. Yup.
There are several ways to deal with that. You could create a changeset
that does not accept is_admin field. But then, you may need to have a
place where user can grant these permissions to others. If this need
arises, you end up with two changeset functions, that you use from
different parts of the code.
While the solution with multiple changesets is okay, I like to introduce custom, tableless schemas to take care of parameters filtering and validation.
We can extract the type casting and form validation from our MyApp.User
module to something like MyApp.Registration.Form and use it in our
workflow and controller.
defmodule MyApp.Registration.Form do
  use Ecto.Schema
  import Ecto.Changeset
  embedded_schema do
    field(:first_name, :string)
    field(:last_name, :string)
    field(:email_address, :string)
    field(:short_bio, :string)
  end
  @required_fields [:email_address, :first_name, :last_name]
  @optional_fields [:short_bio]
  def changeset(attrs) do
    %__MODULE__{}
    |> cast(attrs, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
    |> validate_format(:email_address, ~r/(\w+)@([\w.]+)/)
    |> validate_not_already_registered()
  end
  defp validate_not_already_registered(changeset) do
    with nil <- changeset.errors[:email_address],
         %MyApp.User{} = user <- find_existing_user_by_email(changeset.changes.email_address)
      changeset
      |> add_error(:email_address, "is already taken")
    else
      _ ->
        changeset
    end
  end
  defp find_existing_user_by_email(email) do
    ...
end
defmodule MyApp.Web.RegistrationController do
  ...
  def new(conn, _params) do
    conn
    |> render("new.html", changeset: MyApp.Registration.Form.changeset(%{}))
  end
  ...
end
In workflow itself we have to do one more thing in order the Phoenix
helpers to show validation errors: set the action field on the
changeset manually, but other than that we can just take
changeset.changes from it and feed directly to User.changeset:
 defmodule MyApp.Registration.Workflow do
  def run(registration_params) do
    with %{valid?: true} = form_changeset <- MyApp.Registration.changeset(registration_params),
         db_changeset <- MyApp.User.changeset(%MyApp.User{}, form_changeset.changes),
         user <- MyApp.Repo.insert!(db_changeset),
         :ok <- seed_example_dashboard_data(user),
         :ok <- send_email_confirmation_request(user),
         :ok <- send_admins_new_user_notirication(user) do
           generate_sign_in_token(user)
    else
      %{valid?: false} = changeset ->
        {:error, %{changeset | action: :create}}
      error ->
        error
    end
  end
  ...
endAnd, quite likely we want to alter the form template to specify “as” option for the form helper:
<%= form_for @changeset, Routes.registration_path(@conn, :create), [as: :user, method: :post], fn f -> %>
  ...Final thoughts
We like to keep our controllers and Ecto schemas lean and as much business-logic-free as possible. Both of these parts of application are where your own code deals with infrastructure. It makes sense from code reliability, security and ease of future upgrades to make them as simple as possible - and move your business logic to some other place.
Refactor bravely and refactor often, my Elixir friends :).

Post by Hubert Łępicki
Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.