Getting Started with Phoenix (as a Rails Developer) - Part 1
Elizabeth Karst, Former Developer
Article Categories:
Posted on
Comparing the development paradigm and request-response cycle of Elixir-Phoenix and Rails apps
At Viget we build a lot of Ruby on Rails apps, but recently many Viget developers have been building Elixir apps with Phoenix as well. While Phoenix and Rails have a lot of similarities, the transition from building object-oriented Ruby/Rails apps to functional Elixir/Phoenix apps requires a bit of a mental shift. So for all of the burgeoning Phoenix developers out there, I hope this series provides a solid overview of building Elixir apps with Phoenix and how it differs from building Rails apps.
- Part 1: Development Paradigm and Request / Response Cycle
- Part 2: Routing and Model-View-Controller Architecture
- [Coming Soon] Part 3: Testing and Development Tools
Development Paradigm #
When I started developing Elixir apps, I had to force myself not to approach problems in a Rails-esque way. Though there are many similarities, developing in Elixir is simply different than developing in Ruby. Here are a few patterns I try to keep in mind when tackling a new problem with Elixir.
Functional Programming
Elixir is a functional programming language, which means that data is immutable and object-oriented concepts like classes, inheritance and stateful objects don’t exist. Data is represented as simple structures and primarily transformed through pure functions organized in modules. Because of this simplicity, functional programming tends to allow for shorter, more accurate code that is pleasantly sequential and easy to test and debug. That being said, I often miss the clean, implicit magic of working with Ruby objects when implementing complex business logic.
The Pipe Operator is your Friend
One of my favorite things about Elixir is the pipe operator. Pulling a definition directly from the Elixir docs:
"The |>
symbol is the pipe operator: it takes the output from the expression on its left side and passes it as the first argument to the function call on its right side."
Let’s say you wanted to filter a collection of books down to newly released fiction books with a minimum 4 star rating. Piping allows you to replace nested function calls like with something a big more readable.
# Without Piping
with_minimum_rating( new_releases( search_by_genre(Book, "fiction") ), 4 )
# With Piping
Book |> search_by_genre("fiction") |> new_releases() |> with_minimum_rating(4)
With piping, Book
(the full book collection) is passed in as the first argument of search_by_genre
. The result of search_by_genre
is passed in as the first argument of new_releases
, whose result is passed in as the first argument of with_minimum_rating
. Pipes can also be organized vertically for easy reading:
Book
|> search_by_genre("fiction")
|> new_releases()
|> with_minimum_rating(4)
The pipe operator makes Elixir code feel wonderfully clean and readable and helps me better visualize my code flow.
Further reading:
Forget Conditionals - Embrace Pattern Matching
Rails apps are full of if/else
and case
statements for conditional logic; Elixir apps favor pattern matching and recursion in their stead.
At the highest level it is important to note that =
in Elixir is a match operator, not an assignment operator. It tries to match the right part of an expression to the left part. There is a plethora of writing on general pattern matching in Elixir, so I’ll focus on two scenarios I encounter most frequently in development.
1. Multi-Clause Functions
Let’s say you’re building an app to help users find books to read. Your index view shows a default collection of books and a search box for users to browse with. You update the book list on this index view based on search criteria with a list_books
function.
In Rails you might use a simple if/else
statement to curate your book collection.
def list_books(params)
if params[:search]
# Filter book collection by search criteria
else
# Default book collection
end
end
In Elixir you can use multi-clause functions, which are functions that have multiple definitions with the same arity (arity = number of arguments).
def list_books(%{"search" => search}) do
# Filter book collection by search criteria
end
def list_books(_params) do
# Default book collection
end
Here we have two definitions for the list_books
function. The function whose signature matches the given arguments is the one executed. If a search argument is provided, the first function will be executed. If a search argument is not provided, the later function will be executed. The underscore before the params
argument in the later function signifies that the argument isn’t used in the function, but having it there gives the function the correct arity. It’s good practice to put the most commonly used version of the function below those that are more specific.
Using multi-clause functions for recursion is also preferred to conditionals. Here’s a quick example of how a fibonacci algorithm might look in Elixir:
def fib(0), do: 0
def fib(1), do: 1
def fib(n) do
fib(n-1) + fib(n-2)
end
2. Control Flow
It’s also very common to use pattern matching when creating or updating data in a Phoenix controller action.
A typical Rails #update
controller action might look something like this:
# Rails BooksController: app/controllers/books_controller.rb
def update
book = Book.find(params[:id])
if book.update(book_params)
# If book updates, flash success message and redirect to book show view
else
# Render edit form with error messages
end
end
To achieve the same outcome, Phoenix controllers commonly utilize pattern matching in a case statement:
# Phoenix BookController: lib/my_book_app_web/controllers/book_controller.ex
def update(conn, %{"id" => book_id, "book" => resource_params}) do
book = Book.get_book!(book_id)
case Book.update_book(book, book_params) do
{:ok, book} ->
# If book updates, flash success message and redirect to book show view
{:error, changeset} ->
# Render edit form with error messages
end
end
The return of Book.update_book
(in the case
statement) will be a success tuple, {:ok, book}
, or an error tuple, {:error, changeset}
.
If the book successfully updates, the {:ok, book}
tuple in the case statement will be matched to the {:ok, book}
tuple returned by Book.update_book
, and the book
variable will be bound to the book struct returned by Book.update_book
.
If the book does not update, the {:error, changeset}
tuple in the case statement will be matched to the {:error, changeset}
tuple returned by Book.update_book
, and the changeset
variable will be bound to the changeset returned by Book.update_book
. This allows you to re-render the edit page with the existing user inputs and any errors.
Pattern matching has a wide variety of applications in Elixir apps - it's a joy to practice and apply this new pattern in Elixir projects. Here's some further Reading:
Request / Response Cycle #
One of the fundamental differences between building Elixir apps with Phoenix vs. building Rails apps is how they handle HTTP requests and responses.
Rails uses Action Pack to integrate with Rack, which is middleware that provides an interface between web servers and Ruby apps. Action Pack handles routing by mapping HTTP request URLs to controller actions, and generates HTTP responses through those controller actions (typically with template generated views). With this model, HTTP requests and responses are separated in the middleware stack.
Elixir/Phoenix apps are built around a holistic ‘connection’ struct, referred to as conn
, which provides a unified view of a HTTP request and response. This conn
struct is defined in a Plug.Conn module and stores request and response information. The conn
struct is modified through the app flow (from receiving a request to sending a response) via a pipeline of plugs
. Plugs are simply a series of modules and functions that pass the conn
along through the app flow, modifying or halting it as needed. Note a single conn
struct isn’t actually modified through the app flow because Elixir is an immutable language. Each plug takes in one conn and produces a new, modified conn to pass along to the next plug in the pipeline until a response is sent back to the client.
Here’s an overview of the Phoenix request / response cycle:
- When an HTTP request is received, a
conn
struct is instantiated by a web server handler (Cowboy) and passed to the app Endpoint (Phoenix.Endpoint). - The app Endpoint is the start of the plug pipeline. A new request will always pass through the series of plugs specified in the Endpoint before being routed to a controller (similar to how a request passes through Rack middleware in Rails apps before being routed).
- By default, Endpoint plugs handle things like serving cached files, hot reloading in development, logging, parsing the HTTP request body and storing session info.
- The final plug in the Endpoint is the Router plug. See the hexdocs for a more detailed overview of the default Endpoint plugs.
- The Router Plug defines another series of plug pipeline transformations (more on this in ‘Routing’ in Part 2 of this series) and maps the request URL to a specific controller and action.
- The controller (a plug) and its action (also a plug) fetch any needed data, and render JSON or a template via a view. When a template is rendered, a
send_resp
function is called and an HTTP response is sent to the client.
The beauty of building with Phoenix is that everything involved in the request/response cycle is a plug, and plugs are just composable functions and modules. While middleware like Rack can be difficult to modify or debug (you typically have to build custom middleware to debug or generate logs), plugs can be easily modified, re-organized or isolated for testing and debugging.
Further Reading:
Thanks for reading! You can read more on building Elixir apps with Phoenix vs. Rails in Part 2 and Part 3 of this series:
- Part 2: Routing and Model-View-Controller Architecture
- [Coming Soon] Part 3: Testing and Development Tools