Getting Started with Phoenix (as a Rails Developer) - Part 2

Comparing routing and the MVC architecture of Elixir-Phoenix and Rails apps

Welcome back to this three-part series, which provides an overview of building Elixir apps with Phoenix and how that compares to building Rails apps. Part 2 compares Routing and the Model-View-Controller Architecture for each framework. Be sure to check out Part 1 and Part 3 as well:

Routing

Phoenix routing has a lot in common with Rails routing, but makes controlling how a request is processed easier and more explicit. Before mapping a request URL to a controller and action, the conn is run through some additional plug pipelines (see Part 1 for an overview of conn).

Here’s what a typical Phoenix Router looks like:

# Phoenix Router: lib/my_book_app_web/router.ex
defmodule MyBookApp.Router do
 # Make Phoenix router functions available
 use MyBookApp.Web, :router
 
 # Plug Pipelines
 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
 
 # Scopes
 scope "/", MyBookApp do
   pipe_through :browser
 
   resources "/books", BookController
   get "/books/new_releases", BookController, :new_releases
 end
 
 scope "/api", MyBookApp do
   pipe_through :api
 end
end

The router contains scopes (which define an apps URLs and their associated controller actions) and pipelines (which are used to modify the conn struct before passing it to a controller).

When a request is sent with the url /books, the router will first pipe the request through the :browser pipeline per the pipe_through :browser line in the / scope. The :browser pipeline will fetch flash, fetch session data and run through any other plugs added to that pipeline before the request is dispatched to the BooksController books action.

A separate plug pipeline for api requests can be similarly configured. You can add as many plugs and plug pipelines as you want for a given scope. A user authentication pipeline is a common addition.

In a Rails app, this pipeline functionality is handled in Rack middleware or the controllers themselves. For example, the controller may check user authentication with a before_action, or respond to an API call with render :json. While this allows the Rails Router to be extremely simple (see below), it also means that the logic for determining what data to surface to the user in a view (from auth, to flash, to grabbing the session) is a bit more spread out in the app.

# Rails Router: config/routes.rb
Rails.application.routes.draw do
 resources :books
 get '/books/new_releases', to: 'books#new_releases'
end

Further Reading:

Model-View-Controller Architecture

Rails and Phoenix both embody a Model-View-Controller design pattern, but with slightly different approaches.

Model - ActiveRecord vs. Ecto

While Rails is designed around object-oriented models, it’s somewhat of a misnomer to say Phoenix has models at all. It has a functional data layer most commonly accessed through a domain-specific language called Ecto. Ecto ‘models’ are often referred to simply as ‘structs’ or ‘schemas’.

ActiveRecord allows Rails developers to build objects that handle data persistence, validation and relationships, and automatically infers a schema from the underlying database that maps database tables and columns to objects and their properties.

Ecto also handles data persistence, validation and relationships, but in a different way. Ecto consists of the following:

  1. Schema - a struct used to map fields from a data source to an Elixir struct and create virtual fields that are not persisted. Schemas are built by the developer, not inferred like with ActiveRecord.

  2. Changeset - provides a way to filter and whitelist external parameters, as well as define rules for data validation and transformation. Developers can create different changesets for different scenarios in lieu of using helpers like before_create in Rails.

  3. Repo - a wrapper around a data store that handles data persistence by providing an interface to create, update, delete and query data. Ecto Query provides an Elixir query syntax for retrieving data from a Repo.

Let’s look at an example of what a books model might look like in Rails and Phoenix. To create a ‘Books’ model, you start with a migration in both frameworks (these can be created through simple migration generators - see ‘Development Tools’ in Part 3). The migrations look and behave similarly overall, with a few syntactic differences and the use of a module instead of a class in Phoenix (classes don’t exist in Elixir).

# Rails Migration: db/migrate/XXXX_create_books.rb
class CreateBooks < ActiveRecord::Migration[5.0]
 def change
   create_table :books do |t|
     t.string :title
     t.integer :number_of_pages
     t.references :author
 
     t.timestamps
   end
 end
end
# Phoenix Migration: priv/repo/migrations/XXXX_create_books.exs
defmodule MyBookApp.Migrations.AddBooksTable do
 use Ecto.Migration
 
 def change do
   create table("books") do
     add :title,           :string
     add :number_of_pages, :integer
     add :author_id,       references(:authors)
 
     timestamps()
   end
 end
end

The big difference between Rails and Phoenix migrations is really what happens after you have run them. Rails automatically creates or updates a schema file to map a model to the underlying database table. In Phoenix a Book schema is explicitly built by the developer. While it can feel cumbersome to build schemas manually, it does allow for creating virtual fields that aren’t mapped to the database. A common virtual field is a raw “password” string that can be collected from a user and passed through a changeset, where it is used to create a separate encrypted “password_hash” that is stored in the database.

The Phoenix schema lives in a Book module with a changeset and no other business logic. This module is often referred to as a ‘model’, but it’s really just a module that houses schema and changeset structs - it does not work like a Rails model in any way.

# Phoenix ‘Model’ Struct: lib/my_book_app/books/book.ex
defmodule MyBookApp.Books.Book do
 use Ecto.Schema
 import Ecto.Changeset
 
 schema "books" do
   field :title, :string
   field :number_of_pages, :integer
   belongs_to :author, MyBookApp.Author
   timestamps
 end
 
 @required_fields ~w(title author_id)
 @optional_fields ~w(number_of_pates)
 
 def changeset(user, params \\ %{}) do
   user
   |> cast(params, @required_fields ++ @optional_fields)
   |> validate_required(@required_fields)
   |> unique_constraint(:author_book_constraint, name: :author_book_index)
   |> some_other_validation
 end
 
 defp some_other_validation(changeset) do
   # ...
 end
end

Business logic is defined in Phoenix contexts (modules with groups of related functions). Phoenix controllers and views rely on contexts to gather and update business data via the Repo. Model structs are typically namespaced under the context they are most associated with. Here's an example of what a Phoenix context might look like. It includes functions with business logic around books, like new_releases or top_rated.

# Phoenix Context: lib/my_book_app/books/books.ex
defmodule MyBookApp.Books
 def new_releases do
   # Repo query
 end
end

In summary, a complete book data layer in Phoenix is the combination of the above Book model and Books context. It is common to create contexts that map directly to a ‘model’ like this, but in some cases multiple models may nest under the same context. For example, a single Auth context might support User, Account and Session model structs.

In Rails, a single Book model handles data validations, associations and business logic in one place. The schema that maps this book model to a database table lives in an auto-generated schema.rb file that stores the schema for the entire app. Here’s what the above example might look like in a Rails book model:

# Rails Model: app/models/book.rb
class Book
 belongs_to :author
 
 validates :title, :author, presence: true
 validates_uniqueness_of :title, scope: [:author_id]
 validate :some_other_validation
 
 private
 def some_other_validation
   # custom validation
 end
 
 def self.new_releases
   # ActiveRecord query - find all new releases
 end
end

The division of models and contexts can take a bit of getting used to in Phoenix, but it can be freeing to organize business logic independently of models. It feels a bit like writing a Rails app where all of the business logic lives in shared modules instead of individual models.

Further Reading:

Controller

Controllers are responsible for converting an incoming request into a response by passing data from the model layer to a view. Phoenix and Rails controllers are fairly similar at a high level, but there are a few notable differences:

  1. Everything in Phoenix is handled through plugs. Phoenix controllers are plugs, as are the actions they are composed of. Plugs are also used in place of filters you might use in Rails (e.g. before_action), and can be integrated at any point in the control flow for additional functionality.
  2. Rendering is explicit in Phoenix. Each controller action ends with a redirect or render call that takes the conn, a template and data the template needs. Though you specify a template in a controller action, the controller does not render the template. The controller uses this information to find a corresponding view, which is responsible for surfacing any additional business data and rendering the template. More on this in the “View” section below.
  3. Naming is singular. A BooksController in Rails is a BookController in Phoenix.

Here’s how a simple Phoenix controller for rendering a books index might look:

# Phoenix Controller: lib/my_book_app_web/controllers/book_controller.ex
defmodule MyBookApp.BookController do
 use MyBookApp.Web, :controller
 
 plug :authenticate_resource_class
 
 def index(conn, _params) do
   books = Books.list_books(conn)
   render(conn, :index, books: books)
 end
end

The corresponding controller in Rails appears a bit simpler because there is a good amount of implicit rendering magic going on behind the scenes.

# Rails Controller: app/controllers/books_controller.ex
class BooksController < ApplicationController
 before_action :authorize_resource_class
 
 def index
   @books = Books.list_books(conn)
 end
end

Notice that the authorize_resource_class validation is handled via a plug in Phoenix rather than a before_action like in Rails. Any number of plugs can be added in a similar way depending on how you need to manipulate your conn struct before passing it to a controller action. It’s also worth mentioning that Rails controllers inherit controller functionality through ApplicationController. Elixir doesn’t have a concept of inheritance, so you explicitly include a controller module at the top of each controller (e.g. use MyBookApp.web, :controller).

Further Reading:

View

Rails views != Phoenix views.

  • In Rails, views are HTML files with embedded Ruby code. Rails views are rendered by the controller and are supported by methods in helper modules that make model data easier for the view to use.
  • Phoenix has both views and templates. Phoenix templates are most akin to Rails views - they are HTML files with embedded Elixir code. Phoenix views stand between the controller and the template. Views render templates and provide helper functions to make raw data easier for those templates to use.

In the below example, we have a Phoenix BookView and books index template.

# Phoenix View: lib/my_book_app_web/views/book_view.ex
defmodule MyBookApp.BookView do
 use MyBookApp.Web, :view
 
 def title_and_author(book) do
   "#{book.title}, by #{book.author.full_name}"
 end
end
# Phoenix Template: lib/my_book_app_web/templates/books/index.html.eex
<ul>
 <%= for book <- @books do %>
   <li><%= title_and_author %></li>
 <% end %>
</ul>

The view (BookView) defines functions like title_and_author that the index template uses. Any additional helper functions needed for other book templates (e.g. show or new) would be added to the BookView as well. You specify the template you want the BookView to render in the BookController render call (e.g. render(conn, :index)).

In Rails, a books index view is rendered directly by the BooksController, and helper methods that support that view live in a BookHelper module. Rails will automatically load a BookHelper for any views in a books view directory based on naming conventions.

# Rails View: app/views/books/index.html.erb
<ul>
 <% @books.each do |book| %>
   <li><%= title_and_author %></li>
 <% end %>
</ul>
# Rails Book Helper: app/helpers/book_helper.rb
module BookHelper
 def title_and_author(book)
   "#{book.title}, by #{book.author.full_name}"
 end
end

Structurally, a Rails helper module + view feels fairly similar to a Phoenix view + template. The biggest difference to keep in mind is that the controller is responsible for rendering views in Rails (helpers don't play a role in rendering), while the view is responsible for rendering templates in Phoenix.

Further Reading:

Thanks for reading! You can read more on building Elixir apps with Phoenix vs. Rails in Part 1 and Part 3 of this series:

Elizabeth Karst

Elizabeth is a developer in Boulder, CO. She picked up web development to help write code that deeply reflects human needs, and finds joy in designing databases and optimizing query performance.

More articles by Elizabeth