Skip to the content.

Ruby-on-rails to Phoenix: Porting the blogs tutorial

Preface

There are too many social media comments on how Ruby-on-Rails is best suited for rapid prototyping. Similarly there are high praises to Elixir-Phoenix framework, especially from folks who have worked on both. But there is no easy Getting Started tutorial for Phoenix. In fact Phoenix has quite a high entry barrier before things start making sense. This is easier if someone is coming from Rails world, but for someone starting Phoenix directly from Python-Flask, things look quite complex. I believe beginner documentation is one of the important things holding Phoenix back compared to Ruby on Rails.

So, this is my attempt to recreate Ruby-on-Rails Blog tutorial in Phoenix, with roughly same set of steps.

Here is link to Ruby tutorial I am following and porting.

Installation and setup (Step 1 - 3)

Installation and setup for both Ruby on Rails stack and Elixir-Phoenix stack is equally easy or complex, depending on OS version. For me, using Ubuntu 22.04 in 2024, I can’t use system packages for Ruby on Rails. So I used rbenv to install Ruby 3.2 and then installed rails 8.0 following tutorial instructions.

For Elixir, I used system provided version 1.12.2. For latest Phoenix (especially with LiveView 1.0) I’ll need to use version manager like asdf. But, I’ll stick to using Phoenix version 1.6.6 for this tutorial.

3.2 Creating Blog Application:

For creating new Phoenix project, we have to use mix which is generic Elixir generator.

mix phx.new blogs --database sqlite3

this command will scaffold entire project and set up DB connection. Only difference compared to Rails command is explicit --database sqlite3 specification. By default Ecto which is database handler for Elixir and Phoenix uses PostgreSQL.

4.1 Starting up the server:

mix phx.server

this command starts a development server at Port 4000. I was already running another Phoenix app on this port, so I had to change config/dev.exs -> config :blogs, BlogsWeb.Endpoint -> http: [ip: {127, 0, 0, 1}, port: 4000]. I changed it to 4001 and server started without any issue.

4.2 Adding a controller:

Create a file article_controller.ex in lib/blogs_web/controllers/ folder and add following content:

defmodule BlogsWeb.ArticleController do
  use BlogsWeb, :controller

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

index function of controller renders index.html.heex file as index.html. Add index.html.heex file in lib/blogs_web/templates/article folder (create folder if necessary) and add following content:

<h2>Hello World</h2>

Finally, edit lib/blogs_web/router.ex. In Scope "/", BlogsWeb scope, we need to add route line: get "/articles", ArticleController, :index

For setting root page, I just added get "/", ArticleController, :index line in same scope.

This should render our hello world page at /articles.

Difference with Rails:

This is first step where we can see some difference in approaches. For Rails, I don’t know what all configuration changes are needed to add a controller manually. But it is easy to just run generator command.

For Phoenix mix phx.gen.html generates all views for CRUD operation. But adding a controller is just adding a file in controllers folder.

Another difference is explicit call to “index.html” in render function. This will render embedded html file index.html.heex. Here Phoenix prefers configuration over convention.

One convention part in Phoenix is Article controller’s views are listed in templates/article folder.

6.1 Generating model:

For model generation, mix generator is useful since it creates model struct file and migration file. To generate model, run this command: mix phx.gen.schema Article articles title:string body:string. This will create article.ex file in lib/blogs with schema definition and changeset function. It will also create migration in prov/repo/migrations folder.

6.2 Migrating model:

To apply migrations and create table, run

mix ecto.migrate

So far this is pretty much same what we do in Rails.

6.3 Interacting with the Database using console:

This is the part where Elixir shines over Ruby. Since Elixir is a functional language, all parts of Phoenix syntax can be evaluated independently by calling underlying function.

E.g. in Phoenix/Ecto, a model is represented as a struct. Repo module takes care of interacting with actual database. So, to create a new DB record and insert into DB, we need to follow these steps:

Get an Article by id

Blogs.Repo.get(Blogs.Article, id)

Get all articles

Blogs.Repo.all(Blogs.Article)

6.4 Show list of articles in index page

To show list of articles in a page, we need to change 2 things:

  1. In controller, get list of articles and pass it as data to embedded html.heex page
  2. In embedded html, read this list and create a dom element (li in this case) for each item in the list

For passing list of articles to embedded view, change index function to following code:

def index(conn, _params) do
    render(conn, "index.html", articles: Repo.all(Article))
end

Here we are passing the list of all articles as an assigns map. This map is accessible to embedded view page.

In heex page, we can access this list and render a list using following code:

<ul>
    <%= for entry <- @articles do %>
        <li> 
            <p><%= entry.title %></p> 
            <p> <a> Update </a> </p> 
            <p> <a> Delete </a> </p>
        </li>             
    <% end %>
</ul>    

@articles in above code refers to articles passed to render function. Using HEEX DSL syntax for looping <%= for entry <- @articles do %> we can loop over each article as value of entry.

We’ll update link to update and delete operations later.

Comparison with Rails:

This part is also pretty similar to Rails counterpart. Only noticable difference is that Rails passes value to view by assigning it to a @variable, whereas in Phoenix, render functions takes a Map (Hash or Dict) argument and passes it to the view.

7.1 Showing a single article

At this point, it should be pretty clear on how to show a single article. Basically we need to fetch an article from DB in the controller function and pass it as an assign to embedded view file. In view file, we need to add embedded code to render html tags with article details.

Let’s go through all steps.

  1. Adding route: Add get "/articles/:id", ArticleController, :show line to router.ex. This line says that any url like "/articles/5" should call ArticleController.show function with 5 as a parameter.

  2. Add controller code: In article_controller.ex add show function.
    def show(conn, %{"id" => id}) do
     render(conn, "show.html", article: Repo.get(Article, id))
    end
    

    Here instead of passing params to function and retrieving id inside function body, we can use elixir’s pattern matching %{"id" => id} to get id in declaration line itself. Then we get article from DB and pass it as article key’s value in assigns.

  3. In embedded html generate tag to show article: We have mentioned show.html in controller function. So our embedded files name must be show.html.heex. Add following markup to this file:
<h1> <%= @article.title %> </h1>

<p>
    <%= @article.body %>
</p>

This is straightforward. We take article value passed to view and show title and body.

Comparison with Rails:

Again, difference between Rails and Phoenix is minimal for this part.

7.2 Resourceful routing

Instead of adding custom route for basic CRUD operations, we can add single line to generate all routes. To do so, replace all routes we have added so far in router.ex with this line resources "/articles", ArticleController

Now, to see which all routes are generateed and which controller function will be called for each, we should run the command mix phx.routes.

This will show a table. First column of this table is helper function which can be used to retrieve path from controller function name. Second column is HTTP method. Third is actual route and fourth column indicates controller module and function associated with this route.

E.g. article_path GET /articles/:id BlogsWeb.ArticleController :show This line tells me a GET route “/articles/:id” corresponds to :show method of ArticleController. Also, since route is parameterized, controller function will recieve key-value pair for id in the params argument.

We can use helper function to get route, so that even if route string is modified, the called function remains same. Let’s add a route to create new article in index page. Add following line in the index.html.heex before showing list of articles:

<a href={Routes.article_path(@conn, :new)}> New Article </a>

We use Routes.article_path helper function and pass :new controller name to ensure that we get route string to new article page.

Comparison with rails:

Not much difference.

7.3 Create new article

When listing routes for resources, you may have noticed two different routes for same task.

article_path GET /articles/new BlogsWeb.ArticleController :new and article_path POST /articles BlogsWeb.ArticleController :create

but notice first one is GET route while second one is for POST. Basic idea here is that /new route presents a page with form fields and get all necessary input for creating new resource record, which is Article in our case.

Then, on submitting the form, we should send all fields to POST route where new record will be created and saved to DB.

Corresponding controller functions should have following code:

def new(conn, _params) do
    render(conn, "new.html", formdata: Article.changeset(%Article{}, %{}))
end

def create(conn, %{"article" => article}) do
    %Article{} |> Article.changeset(article) |> Repo.insert
    redirect(conn, to: "/")
end

Notice that create function saves Article and redirects to “/” route.

7.3.1 Creating Form

Let’s create a form and take all inputs needed to create new Article. We’ll need ‘title’ and ‘body’ input fields. While we can create plain HTML form, Phoenix provides us with some utilities which we will generate Article object instead of individual fields after submitting.

Create a new file new.html.heex and following code to create new Article form

<h2> New Article Form </h2>

<.form let={f} for={@formdata} action={Routes.article_path(@conn, :create)}>
    <%= label f, :title %>
    <%= text_input f, :title %>

    <%= label f, :body %>
    <%= textarea f, :body %>

    <%= submit "Save" %>

</.form>

Explanation:

Comparison with Rails:

7.4 Updating an article

Similar to create, there are 3 relevent routes for update.

  1. :update using PATCH
  2. :update using PUT
  3. :edit using GET

The usage is also similar. We use :edit to get to edit form with prefilled fields. On clicking Submit button we send updated values to :update using PATCH or PUT. I’ll pick PATCH method here.

Here are controller methods for update:

def edit(conn, %{"id" => id}) do
    render(conn, "edit.html", formdata: Article.changeset(
        %Article{}, Map.from_struct(Repo.get(Article, id))), id: id)
end

def update(conn, params) do
    IO.inspect(params)
    Repo.get(Article, params["id"]) |> Article.changeset(params["article"])
        |> IO.inspect |> Repo.update
    redirect(conn, to: "/")
end

Compared to creating new article, there few key changes:

  1. We pass id of article to be edited
  2. We fetch article for that id using Repo.get
  3. We create a Map from this article and pass it to Article.changeset function. This function takes 2 arguments, first the Struct for which to create the changeset and second the map from which to fill the values. So we have to do this rather odd way of generating the changeset.
  4. We pass both values and id to view page. id is required to generate form action link.
  5. In update, we first get the article using Repo.get then update it using Article.changeset function and finally save using Repo.update function

View page for edit similar to new page:

<h2>Article</h2>

<.form let={f} for={@formdata} action={Routes.article_path(@conn, :update, @id)}  method="patch">
    <%= label f, :title %>
    <%= text_input f, :title %>

    <%= label f, :body %>
    <%= textarea f, :body %>

    <%= submit "Save" %>

</.form>

We also need to update index page to add update link to this page.

<p> <a href={Routes.article_path(@conn, :edit, entry.id)}> Update </a> </p> 

We added path to update with id of entry.

Comparison with Rails:

Only ORM/DB methods are slightly different. Basic workflow is same for both frameworks.

7.5 Delete an Article

Delete is easier compared to new and update workflows. We simply pass the id to be deleted, delete the article and redirect to index page.

The controller function looks like this:

def delete(conn, params) do
    Repo.get(Article, params["id"]) |> Repo.delete
    redirect(conn, to: "/")
end

And Delete link in index.html.heex should be updated as:

<p> <%= link "Delete", to: Routes.article_path(@conn, :delete, entry.id),
                    method: :delete, data: [confirm: "Are you sure?"] %> </p> 

Notice few differences here:

Comparison with Rails:

Minimal difference. link function arguments are pretty much same in Rails’ link_to function

Conclusion

This concludes the tutorial. My personal objective was to give an immidiate starting point to those migrating from Rails to Phoenix. I know that this is not a self standing tutorial for anybody looking to start Phoenix directly. Perhaps in future I’ll write a complete standalone version of it.