Ruby/Rails vs. Elixir/Phoenix

Ruby/Rails vs. Elixir/Phoenix

This is not a, "Which is better?", post. It is more a way to help other developers, like myself, who have a background in Rails development and are interested in Elixir and Phoenix.

While working my way through converting elFormo, Grok's form processing service, from Ruby/Rails to Elixir/Phoenix, I have found it helpful when others would use a concept in Ruby or Rails as a point of reference for understanding something in Elixir or Phoenix.

Elixir != Ruby and Phoenix != Rails. Even though there may be some similarities between both, it is beneficial to stay open to innovative and often lateral approaches in Elixir/Phoenix.

If you are a Ruby/Rails developer who is interested in adding Elixir/Phoenix to your toolbelt, then I hope this post serves as a helpful starting point.

Programming Paradigm

Object-oriented vs. Functional

If you are coming from a mostly object-oriented background, like me, the first piece of advice I'd offer would be:

Be patient with yourself.

The second bit of advice I'd give:

Imagine writing a Ruby application using only class methods.

Here is an example of what I mean:


class User <, :last)
  def self.full_name(user)
    "#{user.first} #{user.last}"

user ="Bob", "Smith")
# => "Bob Smith"

Notice that we pass user to the full_name method. There are no "object instances" to call methods on in Elixir.

defmodule User do
  defstruct [:first, :last]

  def full_name(user) do
    "#{user.first} #{user.last}"

user = %User{first: "Bob", last: "Smith"}
# => "Bob Smith"

Embrace Pattern Matching

In Ruby, conditionals are predominant, but in Elixir it is more favorable to use pattern matching.

In Ruby, you might see something like this:

def notify_user(user)
  if user.send_emails? do
    # Send emails

In Elixir, we would use pattern matching against the data structure of the user argument:

def notify_user(%User{send_emails?: true}) do
  # Send emails
def notify_user(user), do: user

The above example shows the use of pattern matching within function definitions but we can also use pattern matching to control the flow of logic.

In Rails, this could be considered the equivalent of returning a Result object for handling outcomes beyond just true or false.

In Elixir, you would utilize pattern matching on a function's result. The following is quite common to see in Phoenix controllers:

case MyApp.do_something() do
  {:ok, value}     -> # do stuff ...
  {:error, errors} -> # do different stuff...

Dev Tools

rake vs. mix

In Ruby, there is rake, in Elixir there is mix.

Mix is a build tool that provides tasks for creating, compiling, and testing Elixir projects, managing its dependencies, and more.

Like Rails, Phoenix provides various mix commands to handle common tasks. Most of these commands should feel familiar:

mix my_app
mix phoenix.server
mix ecto.migrate
mix phoenix.gen.html User users email:string
mix test
MIX_ENV=test mix ecto.migrate

irb vs. iex

Ruby gives us the Interactive Ruby Shell (IRB or irb). IEx is Elixir’s Interactive Shell.

Run it within your Elixir project using the command: iex -S mix. The -S tells iex to execute the Mixfile (mix.exs), which handles configuring the project.

Unlike irb, you will have to hit Ctrl-C twice to exit, instead of Ctrl-D.

If you are like me and prefer to have a console session open while developing, usually with some state built up, then the recompile() helper will come in handy. Calling it eliminates having to start a new iex session (and lose all your state) when you've made changes to a file.

Another handy, time-saving tip:

Create a .iex.exs file in your project root to alias your project's modules so you can call, Repo.all, instead of, MyApp.Repo.all.

The first time I fired up iex and wanted to grab the first User in the database, I got hit with:

(UndefinedFunctionError) function MyApp.User.first/0 is undefined or private

Remember, Elixir is functional, so we can't do things like User.first.

Here is the equivalent in Phoenix (technically Ecto, the database layer):

User |> Repo.all |> List.first

That is a lot of typing. Adding a couple convenience functions to my project's Repo module allows me to call Repo.first User. That is better.

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :elformo
  alias __MODULE__

  def first(query) do
    query |> Ecto.Query.first |>

  def last(query) do
    query |> Ecto.Query.last |>

Automated Testing

Minitest vs. ExUnit

Coming from a mostly Ruby Minitest background, I felt right at home with Elixir's ExUnit:

defmodule MyApp.CommentControllerTest do
  # ...

  setup do
    user = insert_user()
    conn = build_conn() |> with_current_user(user)
    {:ok, conn: conn, current_user: user}

  describe "POST /comment" do
    test "when params are invalid", %{conn: conn} do
      # ...
      assert {:ok, comment} == result

As you can see, there is setup, describe and test, which should feel familiar. Although, one thing to note is that you cannot nest describe, which could be considered beneficial because if I find myself wanting to nest describes, I tend to consider it a smell.

The setup runs before each test and passes the returned result as an argument to each test call. This allows you to pattern match on the argument to pull in only the variables you need.

Similarly to Ruby, you can run your tests with

mix test


mix test path/to/file.exs


mix test /path/to/file.exs:28

One difference to note is that there are not many special assert_* functions available in ExUnit. In general, you will use assert/refute most of the time. This is because the test failure output is so helpful:

Comparison (using ==) failed in:
code: some_fun() == 10
lhs:  13
rhs:  10

As you can see, the output gives you the left-hand side (lhs) and the right-hand side (rhs) of the statement you are asserting against, which makes the failure very clear and eliminates the need for specialized assert functions.

Database Layer

ActiveRecord vs. Ecto

Ecto is a domain specific language for writing queries and interacting with databases in Elixir.

Phoenix uses Ecto for its database layer, and it has four main components:

Even though Ecto has similar concepts and behavior, like validations and queries, it is a different animal compared to ActiveRecord. In fact, I don't think you can really compare them. So let us just touch a little bit on what Ecto gives you.

Ecto - Schema

defmodule MyApp.User do
  use Ecto.Schema

  schema "users" do
    field :username, :string
    field :encrypted_password, :string
    field :email, :string
    field :confirmed, :boolean, default: false
    field :password, :string, virtual: true
    field :password_confirmation, :string, virtual: true

As you can see, schemas allow you to define the field names and data types. You can also have virtual fields which are useful for data required to run validations but that does not need to be stored in the database. For example, a password confirmation field or even fields within a contact form. For a contact form, you would simply leave the schema name blank and use all virtual fields:

defmodule MyApp.Inquiry do
  use Ecto.Schema

  schema "" do
    field :name, :string, virtual: true
    field :email, :string, virtual: true
    field :comment, :binary, virtual: true

In Chris McCord's 2016 Elixir/Phoenix Conf keynote, he emphasizes moving away from using the term "model". I am not aware of what should be used in place of "model", so for now I am sticking with "schema", since that is what Ecto uses.

Ecto - Changeset

Changesets handle data casting, validation, and filtering and can be used to accomplish Rails callback behavior by defining separate changeset functions for different contexts, e.g. user registration, password update, insert/update.

You also use changesets to handle calculations or transforming data, for example encrypting a password, which is typically done in Rails with a before_save callback.

defmodule MyApp.User do
  import Ecto.Changeset

  schema "users" do
    # ...

  @required_fields ~w(username encrypted_password email)
  @optional_fields ~w()

  def changeset(user, params \\ %{}) do
    |> cast(params, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
    |> encrypt_password
    |> unique_constraint(:username)

  defp encrypt_password(changeset) do
    # ...

Ecto - Repo

The Repo is the database wrapper and it is used for executing inserts, updates, deletes and queries:

Repo.aggregate(User, :count, :id)

query = from u in "users",
          where: == "",

Phoenix will automatically generate your Repo module for you, which you can use to add convenience functions, like first and last, which I mentioned earlier on.

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :example_app

As I mentioned, Ecto also has a Query component, but I haven’t had much experience with it yet, so I'll just direct you to the documentation if you want to learn more.

Also, if you are used to Rails preloading associations for you, you will probably be disappointed (I was, at first) to find out that in Ecto, you must manually preload associations.

Repo.get(User, id) |> Repo.preload([:profile])

Even though this results in slightly more code, the explicitness (which Elixir favors) forces you to think about the data you are retrieving, which I actually now find to be beneficial.

Controllers & View

Controllers vs. ... Controllers

There is not much difference between a Rails controller and a Phoenix controller. However, there are a couple things worth mentioning:

# web/router.ex
resources "/users", UserController

# web/controllers/user_controller.ex
defmodule MyApp.UserController do
  use MyApp.Web, :controller

  alias MyApp.{Repo, User}

  def index(conn, _params) do
    users = Repo.all(User)
    render(conn, "index.html", users: users)

  def new(conn, _params), do: #...
  def show(conn, %{"id" => id}), do: #...
  def edit(conn, %{"id" => id}), do: #...
  def create(conn, %{"user" => user_params}), do: #...
  def update(conn, %{"id" => id, "user" => user_params}), do: #...
  def delete(conn, %{"id" => id}), do: #...

Views vs. Views + Templates

A Rails view != a Phoenix view.

A Phoenix view, which is more equivalent to a Rails view helper, handles rendering the template and can also be the place to define helper functions for decorating and formatting the data in your templates.

# web/views/user_view.ex
defmodule MyApp.UserView do
  use MyApp.Web, :view

  def full_name(user) do
    "#{user.first} #{user.last}"

Phoenix templates are more comparable to Rails views. They use EEx (embedded Elixir), which is very similar to ERB.

# web/templates/users/index.html.eex
<%= for user <- @users do %>
    <td><%= full_name(user) %></td>
<% end %>

One important thing to note:

Lines containing conditionals and loops in templates need to use, <%=, not <%. Otherwise, whatever is nested within them will not be displayed.


Rack vs. Plug

Since they describe it so well, I'll just copy directly what the Phoenix docs have to say about Plug:

Plug is a specification for composable modules in between web applications. It is also an abstraction layer for connection adapters of different web servers. The basic idea of Plug is to unify the concept of a "connection" that we operate on. This differs from other HTTP middleware layers such as Rack, where the request and response are separated in the middleware stack.

Based on my current understanding, you can implement plugs at the router level, for application-wide functionality, e.g. authentication, as well as at the controller level, for more specialized functionality, e.g. authorization or reducing duplication between actions.

Using a plug at the router level (module plug) looks like the example below. You define a module that implements two functions:

The call() function must return the connection for the purpose of composability.

Then in your router, you can add your plugs to a pipeline through which you can pipe your routes.

defmodule MyApp.Plugs.Auth do
  def init([]), do: false
  def call(conn, _opts), do: conn

defmodule MyApp.Router do
  # ...
  pipeline :auth do
    plug MyApp.Plugs.Auth

  scope "/", MyApp do
    pipe_through [:auth]
    resources "/users", UserController

You can also define plugs at the controller level (function plug), and specify to which actions the plug should be applied:

defmodule MyApp.UserController do
  plug :authenticate when action in [:create, :update, :delete]

  defp authenticate(conn, opts), do #...

Dependency Management

RubyGems vs. Hex Packages

Hex is the package manager for the Erlang ecosystem.

Installing project dependencies in Elixir/Phoenix is just as easy as it is in Ruby/Rails.

To install new packages, simply add the package name and version to your mix.exs file and run mix deps.get.

defp deps do
    {:phoenix, "~> 1.2.1"},
    {:phoenix_pubsub, "~> 1.0"},
    {:phoenix_ecto, "~> 3.0"},
    {:postgrex, ">= 0.0.0"},
    {:phoenix_html, "~> 2.6"},
    # ...

One small, slightly silly tip:

Append the keyword "package" to your google searches for finding Hex... packages. Not gems, packages.


There is not much in the way of comparisons when it comes to debugging in Rails vs. Phoenix.

Elixir comes with IEx and you can call IEx.pry anywhere in your application, just like you would in Ruby/Rails with binding.pry.

Of course, you can always reach for IO.puts and IO.inspect, which will show up in the web server output.

Two things to note:

Once you've done those two things, you can then refresh your browser, check your iex session and, if you are used to pry in Ruby, you will find yourself right at home.

Once you are done debugging, call respawn() to continue/finish the request.

One Last Tip

Use iex -S mix generously.

I recall in a recent Rails project, trying to inspect some monstrous object with pages and pages of state. I have yet to come across any overwhelmingly large data structures in Elixir/Phoenix. The function return values I have inspected, so far, have been very simple and straightforward to understand, at a glance.

For example, here is the data structure of an Ecto Changeset:

iex(1)> Form.insert_changeset(%Form{}, %{name: "Contact Us"})
Ecto.Changeset<action: nil,
changes: %{name: "Contact Us"},
errors: [user_id: {"can't be blank", [validation: :required]}],
data: #MyApp.Form<>,
valid?: false>


Of course, there is a lot I didn't cover and I am still not entirely done converting the elFormo application. So, I am certain there is much more to learn and possibly a "part two" blog post in the near future.

If you want more information, the documentation for everything I have linked to in this post is excellent. In addition, here are some other resources I have personally found helpful:

Categories: Software Development | Tags: Elixir, Phoenix, Ruby, Rails

Portrait photo for Lauren Fackler Lauren Fackler

Lauren's experience includes backend/frontend web development and UI design. She really enjoys working with Ruby and Ruby on Rails and also loves to design, refactor and tidy up code.


Let us help you

We provide a free consultation to discover competitive advantages for your business. Contact us today to schedule an appointment.