Back-end9-minute read

Meet Phoenix: A Rails-like Framework for Modern Web Apps on Elixir

In back-end software development, increased productivity often comes at the cost of performance. In this article, Toptal Software Engineer Eduardo Bautista shows us how the Phoenix framework builds on the familiar concepts from the Rails world, and makes it even easier to build robust concurrent applications without compromising performance.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

In back-end software development, increased productivity often comes at the cost of performance. In this article, Toptal Software Engineer Eduardo Bautista shows us how the Phoenix framework builds on the familiar concepts from the Rails world, and makes it even easier to build robust concurrent applications without compromising performance.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Eduardo Bautista

Eduardo Bautista

Eduardo worked for several successful startups, and has extensive experience with international clients, including enterprise clients.

Share

The Phoenix framework has been growing with popularity at a quick pace, offering the productivity of frameworks like Ruby on Rails, while also being one of the fastest frameworks available. It breaks the myth that you have to sacrifice performance in order to increase productivity.

So what exactly is Phoenix?

Phoenix is a web framework built with the Elixir programming language. Elixir, built on the Erlang VM, is used for building low-latency, fault-tolerant, distributed systems, which are increasingly necessary qualities of modern web applications. You can learn more about Elixir from this blog post or their official guide.

If you are a Ruby on Rails developer, you should definitely take an interest in Phoenix because of the performance gains it promises. Developers of other frameworks can also follow along to see how Phoenix approaches web development.

Meet Phoenix on Elixir: A Rails-like Framework for Modern Web Apps

In this article we will learn some of the things in Phoenix you should keep in mind if you are coming from the world of Ruby on Rails.

Unlike Ruby, Elixir is a functional programming language, which is probably the biggest difference that you may have to deal with. However, there are some key differences between these two platforms that anyone learning Phoenix should be aware of in order to maximize their productivity.

Naming conventions are simpler.

This is a small one, but it’s easy to mess up if you are coming from Ruby on Rails.

In Phoenix, the convention is to write everything in the singular form. So you would have a “UserController” instead of a “UsersController” like you would in Ruby on Rails. This applies everywhere except when naming database tables, where the convention is to name the table in the plural form.

This might not seem like a big deal after learning when and where to use the plural form in Ruby on Rails, but it was a bit confusing at first, when I was learning to use Ruby on Rails, and I am sure it has confused many others as well. Phoenix gives you one less thing to worry about.

Routing is easier to manage.

Phoenix and Ruby on Rails are very similar when it comes to routing. The key difference is in how you can control the way a request is processed.

In Ruby on Rails (and other Rack applications) this is done through middleware, whereas in Phoenix, this is done with what are referred to as “plugs.”

Plugs are what you use to process a connection.

For example, the middleware Rails::Rack::Logger logs a request in Rails, which in Phoenix would be done with the Plug.Logger plug. Basically anything that you can do in Rack middleware can be done using plugs.

Here is an example of a router in Phoenix:

defmodule HelloWorld.Router do
  use HelloWorld.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", HelloWorld do
    pipe_through :browser

    get "/", HelloController, :index
  end

  scope "/api", HelloWorld do
    pipe_through :api
  end
end

First, let’s look at the pipelines.

These are groups of plugs through which the request will travel. Think of it as a middleware stack. These can be used, for example, to verify that the request expects HTML, fetch the session, and make sure the request is secure. This all happens before reaching the controller.

Because you can specify multiple pipelines, you can pick and choose which plugs are necessary for specific routes. In our router, for example, we have a different pipeline for pages (HTML) and API (JSON) requests.

It doesn’t always make sense to use the exact same pipeline for different types of requests. Phoenix gives us that flexibility.

Views and templates are two different things.

Views in Phoenix are not the same as views in Ruby on Rails.

Views in Phoenix are in charge of rendering templates and providing functions that make raw data easier for the templates to use. A view in Phoenix most closely resembles a helper in Ruby on Rails, with the addition of rendering the template.

Let’s write an example that displays “Hello, World!” in the browser.

Ruby on Rails:

app/controllers/hello_controller.rb:

class HelloController < ApplicationController
  def index
  end
end

app/views/hello/index.html.erb:

<h1>Hello, World!</h1>

Phoenix:

web/controllers/hello_controller.ex:

defmodule HelloWorld.HelloController do
  use HelloWorld.Web, :controller

  def index(conn, _params) do
    render conn, "index.html"
  end
end

web/views/hello_view.ex:

defmodule HelloWorld.HelloView do
  use HelloWorld.Web, :view
end

web/templates/hello/index.html.eex:

<h1>Hello, World!</h1>

These examples both display “Hello, World!” in the browser but have some key differences.

First, you have to explicitly state the template that you wish to render in Phoenix unlike in Ruby on Rails.

Next, we had to include a view in Phoenix which stands in between the controller and the template.

Now, you may be wondering why we need a view if it’s empty? The key here is that, under the hood, Phoenix compiles the template into a function roughly equal to the code below:

defmodule HelloWorld.HelloView do
  use HelloWorld.Web, :view

  def render("index.html", _assigns) do
    raw("<h1>Hello, World!</h1>")
  end
end

You can delete the template, and use the new render function, and you will get the same result. This is useful when you just want to return text or even JSON.

Models are just that: data models.

In Phoenix, models primarily handle data validation, its schema, its relationships with other models, and how it’s presented.

Specifying the schema in the model may sound weird at first, but it allows you to easily create “virtual” fields, which are fields that are not persisted to the database. Let’s take a look at an example:

defmodule HelloPhoenix.User do
  use HelloPhoenix.Web, :model

  schema "users" do
    field :name, :string
    field :email, :string
    field :password, :string, virtual: true
    field :password_hash, :string
  end
end

Here we define four fields in the “users” table: name, email, password, and password_hash.

Not much is interesting here, except for the “password” field which is set as “virtual.”

This allows us to set and get this field without saving the changes to the database. It’s useful because we would have logic to convert that password into a hash, which we would save in the “password_hash” field and then save that to the database.

You still need to create a migration; the schema in the model is needed because the fields are not automatically loaded into the model like in Ruby on Rails.

The difference between Phoenix and Ruby on Rails is that the model does not handle the persistence to the database. This is handled by a module called “Repo” which is configured with the database information, as shown in the example below:

config :my_app, Repo,
  adapter: Ecto.Adapters.Postgres,
  database: "ecto_simple",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"

This code is included in the environment specific configuration files, such as config/dev.exs or config/test.exs. This allows us to then use Repo to perform database operations such as create and update.

Repo.insert(%User{name: "John Smith", example: "john@example.com"}) do
  {:ok, user} -> # Insertion was successful
  {:error, changeset} -> # Insertion failed
end

This is a common example in controllers in Phoenix.

We provide a User with a name and an email and Repo attempts to create a new record into the database. We can then, optionally, handle the successful or failed attempt as shown in the example.

To understand this code better, you need to understand pattern matching in Elixir. The value returned by the function is a tuple. The function returns a tuple of two values, a status, and then either the model or a changeset. A changeset is a way to track changes and validate a model (changesets will be discussed in the next section).

The first tuple, from top to bottom, that matches the pattern of the tuple returned by the function that attempted to insert the User into the database, will run its defined function.

We could have set a variable for the status instead of an atom (which is basically what a symbol is in Ruby), but then we would match both successful and failed attempts and would use the same function for both situations. Specifying which atom we want to match, we define a function specifically for that status.

Ruby on Rails has the persistence functionality built into the model through ActiveRecord. This adds more responsibility to the model and can sometimes make testing a model more complex as a result. In Phoenix, this has been separated in a way that makes sense and prevents bloating every model with persistence logic.

Changesets allow clear validation and transformation rules.

In Ruby on Rails, validating and transforming data can be the source of hard to find bugs. This is because it’s not immediately obvious when data is being transformed by callbacks such as “before_create” and validations.

In Phoenix, you explicitly do these validations and transformations using changesets. This is one of my favorite features in Phoenix.

Let’s take a look at a changeset by adding one to the previous model:

defmodule HelloPhoenix.User do
  use HelloPhoenix.Web, :model

  schema "users" do
    field :name, :string
    field :email, :string
    field :password, :string, virtual: true
    field :password_hash, :string
  end
  
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :email, :password])
    |> validate_required([:email, :password])
  end
end

Here the changeset does two things.

First, it calls the “cast” function which is a whitelist of permitted fields, similar to “strong_parameters” in Ruby on Rails, and then it validates that the “email” and “password” fields are included, making the “name” field optional. This way, only the fields you allow can be modified by users.

The nice thing about this approach is that we aren’t limited to one changeset. We can create multiple changesets. A changeset for registration and one for updating the user is common. Maybe we only want to require the password field when registering but not when updating the user.

Compare this approach to what is commonly done in Ruby on Rails, you would have to specify that the validation should be run only on “create”. This sometimes makes it hard in Rails to figure out what your code is doing once you have a complex model.

Importing functionality is straightforward, yet flexible.

Much of the functionality of a library called “Ecto” is imported into the models. Models usually have this line near the top:

use HelloPhoenix.Web, :model

The “HelloPhoenix.Web” module is located in “web/web.ex”. In the module, there should be a function called “model” as follows:

def model do
  quote do
    use Ecto.Schema

    import Ecto
    import Ecto.Changeset
    import Ecto.Query
  end
end

Here you can see what modules we are importing from Ecto. You can remove or add any other modules that you want here and they will be imported into all models.

There are also similar functions such as “view” and “controller” which serve the same purpose for the views and controllers, respectively.

The quote and use keywords might seem confusing. For this example, you can think of a quote as directly running that code in the context of the module that is calling that function. So it will be the equivalent to having written the code inside of quote in the module.

The use keyword also allows you to run code in the context of where it’s called. It essentially requires the specified module, then calls the __using__ macro on the module running it in the context of where it was called. You can read more about quote and use in the official guide.

This really helps you understand where certain functions are located in the framework and helps reduce the feeling that the framework is doing a lot of “magic”.

Concurrency is at the core.

Concurrency comes for free in Phoenix because it is a main feature of Elixir. You get an application that can spawn multiple processes and run on multiple cores without any worries about thread safety and reliability.

You can spawn a new process in Elixir this simply:

spawn fn -> 1 + 2 end

Everything after spawn and before end will run in a new process.

In fact, every request in Phoenix is handled in its own process. Elixir uses the power of the Erlang VM to bring reliable and efficient concurrency “For free.”

This also makes Phoenix a great choice for running services that use WebSockets, since WebSockets need to maintain an open connection between the client and the server (which means you need to build your application so that it can handle possibly thousands of concurrent connections).

These requirements would add a lot of complexity to a project built on Ruby on Rails, but Phoenix can meet these requirements for free through Elixir.

If you want to use WebSockets in your Phoenix application, you are going to need to use Channels. It is the equivalent of ActionCable in Ruby on Rails, but less complex to set up because you don’t need to run a separate server.

Phoenix makes building modern web apps painless.

While we’ve largely been focusing on the differences, Phoenix does have some things in common with Ruby on Rails.

Phoenix roughly follows the same MVC pattern as Ruby on Rails, so figuring out what code goes where should not be difficult now that you know about the main differences. Phoenix also has similar generators as Ruby on Rails for creating models, controllers, migrations, and more.

After learning Elixir, you will slowly approach Ruby on Rails levels of productivity once you are more comfortable with Phoenix.

The few times I feel not as productive is when I encounter a problem that was solved by a gem in Ruby, and I can’t find a similar library for Elixir. Luckily, these gaps are slowly being filled by the growing Elixir community.

The differences in Phoenix help solve a lot of the pain that came with managing complex Ruby on Rails projects. While they don’t solve all issues, they do help push you in the right direction. Choosing a slow framework because you want productivity is no longer a valid excuse, Phoenix lets you have both.

Hire a Toptal expert on this topic.
Hire Now

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

Join the Toptal® community.