Comparing Rails and Phoenix: Part II

Written by: Leigh Halliday

In the first post of this two-part series, we touched on generating a new application and talked about the entry point to each application: the Router. We also discussed at a high level about how Phoenix apps can fit into larger OTP applications.

In this post, we will be looking at the Model, View, and Controller, the parts that comprise a typical MVC framework.

The Model

The Model, that place where your application's business logic tends to reside and where the schema is defined to be able to talk to the database. Where rows and columns are converted to objects or structs. Here, we'll explore some of the differences in the model layer between Rails and Phoenix.

The model in Rails

There is a lot of "magic" going on in Rails, and I love it! I purposefully put that word magic in quotes because it isn't really magic. It's logic that is defined in the ActiveRecord::Base class that we get access to as soon as we create a class which inherits from it.

Let's take a look at our User class:

class User < ApplicationRecord
  has_many :tweets
  has_many :notifications
  validates :email, :name, :username, :bio, presence: true
  def to_param

And with that little code, we suddenly have the ability to query the users table in our database, to validate each object, and to reach out to related objects that we've defined through the two has_many lines.

In Rails, we use the model class to find and create instances of that same class. User.find_by(username: "leigh") goes to the database and returns us an instance of the User class that matches the query we gave.

We also don't have to tell Rails which columns this table has. It figures all of that out on its own. ActiveRecord is an ORM (Object Relational Mapper); mapping relational data in the database to objects in Ruby. You may wonder why I'm going into detail about things that to a seasoned Rails developer are second nature -- that is because in Phoenix it's done differently.

Database migrations in Rails

Here are the migrations which were generated with the scaffold command from earlier in the article.

class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :email
      t.string :name
      t.string :username
      t.string :bio
    add_index :users, :email, unique: true
    add_index :users, :username, unique: true

The model in Phoenix

The model layer in Phoenix is actually handled by a great library called Ecto. As we take a look at the User model in Phoenix, you'll immediately notice some differences. The first is that we define the different columns and relationships it has in a schema block. Ecto requires you to be a little bit more explicit about your database schema and how it maps to your Ecto model.

defmodule TwitterPhoenix.User do
  use TwitterPhoenix.Web, :model
  schema "users" do
    field :name, :string
    field :username, :string
    field :email, :string
    field :bio, :string
    has_many :tweets, TwitterPhoenix.Tweet
    has_many :notifications, TwitterPhoenix.Notification
  @required_fields ~w(name username email bio)
  @optional_fields ~w()
  @doc """
  Creates a changeset based on the `model` and `params`.
  If no params are provided, an invalid changeset is returned
  with no validation performed.
  def changeset(model, params \\ :empty) do
    |> cast(params, @required_fields, @optional_fields)
    |> unique_constraint(:username)
    |> unique_constraint(:email)

Thankfully, most of that is written for us when we generated the scaffolding for our application, so don't be afraid or turned off by its verboseness. In fact, it is nice to be able to look at your model and immediately see how it is structured.

Because Elixir is a functional, immutable language, there are no "objects" as you find in Ruby. Validations and casting of the data are defined inside the changeset method. You provide the existing data in addition to the data you are changing, and it will produce an Ecto.Changeset struct which contains all of the information needed to validate and save the record.

Unlike Rails where you use the class and object instances to interact with the database, all of that in Ecto is done through a Repo module. If we look at the update action of the UserController, we'll see two different interactions with Repo along with some interactions with the changeset.

def update(conn, %{"id" => username, "user" => user_params}) do
  # Find a user by its username
  user = Repo.get_by!(User, username: username)
  # Generate a changeset using existing user and incoming user_params
  changeset = User.changeset(user, user_params)
  # Attempt to save changeset to the database
  case Repo.update(changeset) do
    {:ok, user} ->
      |> put_flash(:info, "User updated successfully.")
      |> redirect(to: user_path(conn, :show, user.username))
    {:error, changeset} ->
      render(conn, "edit.html", user: user, changeset: changeset)

First we use the Repo.get_by! method to find a User by its username. We then produce a changeset by providing the existing user and the form data (found in user_params).

With the changeset we can now use the Repo.update method to attempt to save these changes to the database. Handling the result is done in Elixir style pattern matching rather than the Rails style if statements, but the goal is the same.

Database migrations in Phoenix

Here is what migrations look like in Phoenix. They look quite similar to the ones in Rails and should be instantly understandable to someone coming to Phoenix from Rails.

defmodule TwitterPhoenix.Repo.Migrations.CreateUser do
  use Ecto.Migration
  def change do
    create table(:users) do
      add :name, :string
      add :username, :string
      add :email, :string
      add :bio, :string
    create unique_index(:users, [:username])
    create unique_index(:users, [:email])

The Controller

Controllers sit sort of in the middle of a web request. We get to the controller by way of the routing, and it's the controller's job to basically convert incoming request (and its params) into a response. It does so by interacting with the Model layer and then passing that data to a view who presents the data as HTML or JSON.

We're going to be looking at the TweetsController, more specifically at the index action, which shows the latest tweets for a specific user, in this case.

Controllers in Rails

With controllers in Rails, we typically end up in a method referred to as the action. This is the method that the router calls and whose job it is to gather data from the model and pass it to the view. Let's take a look at the index action:

class TweetsController < ApplicationController
  before_action :set_user
  def index
    @tweets = @user.tweets.latest
  def set_user
    @user = User.find_by(username: params[:user_id])

The index method is quite small, but what's actually going on here?

In Rails, you can have before_action filters which happen before the action actually gets called. These can set data (such as the @user variable) and perform security authorization checks, among other things. We declared that we wanted a before_action to take place at the top of the controller and defined the actual method itself further below.

Inside the set_user method, we're using data that has come to us via the URL. The path which maps to this action looks like /users/:user_id/tweets, so if we want to grab this user's latest tweets, we first need to find the user, using their ID (username in this case).

In Rails, we have access to URL params, GET params, and POST data all through a single object, the params. This may look like just a hash, but it's a little more complicated than that. We can actually use it to validate our incoming parameters using StrongParameters, which can require certain fields to exist and permit (whitelist) others.

In this case, we are just grabbing the user_id. Just one note about this is that the user_id here is actually their username. It could have been redefined to username in the routing, but I just went with the default naming convention.

Variables are passed to the view by setting them as instance variables (@ variables) rather than local ones. In this case, rendering happens implicitly by Rails, which knows to look for the index.html template inside the views/tweets folder. We can choose to call the render method if we want more control over the process or something different from the default behavior.

Controllers in Phoenix

If I were to describe the main differences between controllers in Rails and those in Phoenix, I would say that they are a little simpler and a bit more explicit in Phoenix.

The first thing to note in Phoenix is that there is only a single concept which weaves its way through controllers, and that is the same we saw in routing: namely, the plug. There are no filters, no actions, just plugs.

It may appear very similar because of some nice macros that help define what is commonly done in filters and actions, but these are just plugs. Also, because Elixir is a functional language, there are no objects we have access to, like the request or params objects which we have in Rails.

There is a single conn struct which is passed from plug to plug, containing all of the information of the entire request, including the params. We add or modify this struct as it flows from one plug to another. Take a look at our controller below (I have again removed code which doesn't relate to the specific use-case):

defmodule TwitterPhoenix.TweetController do
  use TwitterPhoenix.Web, :controller
  alias TwitterPhoenix.{UserTweets, User, Tweet}
  plug :load_user
  def index(conn, _params) do
    tweets = UserTweets.latest(conn.assigns[:user])
    render(conn, "index.html", tweets: tweets)
  defp load_user(conn, _) do
    user = Repo.get_by!(User, username: conn.params["user_id"])
    assign(conn, :user, user)

It looks similar to what we saw in Rails. We define what would be a before_action filter (a plug) called load_user. This happens before the index method is called, and we can access the user_id (username) from the conn variable to find the user. We then add its information to the conn struct and send it on its way to the next plug, which happens to be the index method.

In the index method, we have to be explicit about rendering the view that we want, passing the variables we want it to have access to.

The View

We've made it to the point of the request/response life-cycle where we need to render some HTML that will be returned to the user.

Views in Rails

Views in Rails are pretty self-explanatory. They have access to instance variables which were set in the controller that rendered it, along with a number of other helper methods which come from Rails or which were defined by the user. The default views in Rails are written in ERB (Embedded Ruby), allowing us to mix HTML with Ruby logic.

html <tbody> <% @tweets.each do |tweet| %> <tr> <td><%= tweet.body %></td> <td><%= tweet.retweet_count %></td> <td><%= tweet.like_count %></td> <td><%= link_to 'Show', user_tweet_path(@user, tweet) %></td> </tr> <% end %> </tbody>

Views in Phoenix

Things change quite a bit when we enter Phoenix.

Here we don't go from the controller directly to where we write HTML. Phoenix has split things up into "views" and "templates."

Views are modules who have the job of rendering the template and providing methods to assist with rendering the data. You can almost think of them as "presenters" for the templates, where you might want to present the data differently if you are rendering HTML vs JSON. It provides a bit of separation between the controller who finds the data, ensures the user is authenticated and authorized, and the template which renders the output.

In our case, we aren't doing anything special with the data at the moment, so it is a module with a single method that helps us display the user's name:

defmodule TwitterPhoenix.TweetView do
  use TwitterPhoenix.Web, :view
  def username(conn) do

The template which produces the HTML actually ends up looking remarkably similar to how things looked in Rails:

html <tbody> <%= for tweet <- @tweets do %> <tr> <td><%= tweet.body %></td> <td><%= tweet.retweet_count %></td> <td><%= tweet.like_count %></td> <td class="text-right"> <%= link "Show", to: user_tweet_path(@conn, :show, username(@conn), tweet), class: "btn btn-default btn-xs" %> </td> </tr> <% end %> </tbody>


With the imminent release of Rails 5, both Rails and Phoenix have support for websockets and channels by default. This wasn't the case prior to Rails 5, and you might say that the implementation in Phoenix is still a stronger one.

You may have seen the article that talks about how the Phoenix team achieved two million connections on a single (albeit very large) server. I have yet to see a similar comparison in Rails, although my gut instinct tells me it wouldn't be able to achieve as many (although I'd love to be proven wrong). That being said, there are very few applications which require this sort of scaling. If you are one of them, congratulations, that's awesome! For the majority of websites, Rails should perform just fine, and it's a great addition to the framework.

ActionCable (channels) in Rails requires either Redis or PostreSQL to handle the pub/sub nature of channels. You are welcome to use those with channels in Phoenix as well, but they aren't required due to the concurrent-by-default nature of the language.


In this article, I tried to touch on the main components of an MVC web framework. As many readers will have realized, there is more that I have left out than I was able to include!

We didn't get to talk about ecosystems, testing, async processing through queues (or natively in Elixir), email, caching, among other things. But hopefully what I was able to do was to point out a few of the commonalities between these powerful frameworks, as well as other areas where they might differ in terms of their philosophy and/or implementation.

If you are interested in taking a look at the code I used in these examples, here are links to the Phoenix app and the Rails app. If you haven't tried Phoenix or the Elixir language before, please do! You won't be disappointed.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.