Elixir Phoenix Framework — Tutorial To Build a Blog in 15 Minutes

Jakub Cieślar

Elixir Phoenix Framework Tutorial - how to build a blog in 15 minutes

I suppose everyone can recall blog posts about building your own blog in 15 minutes with Ruby on Rails. Building a simple blog post page with Rails was as easy as writing hello world in any other language. Nowadays, however, there are more and more articles Phoenix framework tutorials and it looks like the Ruby world has started to fall in love with Elixir. Because of that, I took up the challenge and decided to check out how easy (or difficult ;-)) it is to write a blog using the Phoenix framework.

I would like to encourage you to go through the following Elixir tutorial and the Phoenix web framework tutorial as well, or checking out a Youtube tutorial by Tensor Programming:

And for those old-school folks who still fancy reading, I recommend 'Programming Elixir' by Dave Thomas.

The main aim of this article is to draw your attention to Elixir and the Phoenix on the whole. I'm not going to provide you with a short tutorial and deep explanation of how it works. I would just like to show you how we can write something as simple as a blog with Elixir and the Phoenix framework.

Preparations — install Elixir and Phoenix

First of all, you will need to install Elixir. Next, install Hex package manager and Phoenix:

$ mix local.hex
$ mix archive.install

These steps are thoroughly described here.

Step 1 — create a project, dependencies, and compile

Let's create a project named 'blog_phoenix' using:

$ mix phoenix.new blog_phoenix

You can see that the following files were created:

* creating blog_phoenix/config/config.exs
* creating blog_phoenix/config/dev.exs
* creating blog_phoenix/config/prod.exs
* creating blog_phoenix/config/prod.secret.exs
* creating blog_phoenix/config/test.exs
* creating blog_phoenix/lib/blog_phoenix.ex
* creating blog_phoenix/lib/blog_phoenix/endpoint.ex
* creating blog_phoenix/test/controllers/page_controller_test.exs
* creating blog_phoenix/test/views/error_view_test.exs
* creating blog_phoenix/test/views/page_view_test.exs
* creating blog_phoenix/test/views/layout_view_test.exs
* creating blog_phoenix/test/support/conn_case.ex
* creating blog_phoenix/test/support/channel_case.ex
* creating blog_phoenix/test/test_helper.exs
* creating blog_phoenix/web/channels/user_socket.ex
* creating blog_phoenix/web/controllers/page_controller.ex
* creating blog_phoenix/web/templates/layout/app.html.eex
* creating blog_phoenix/web/templates/page/index.html.eex
* creating blog_phoenix/web/views/error_view.ex
* creating blog_phoenix/web/views/layout_view.ex
* creating blog_phoenix/web/views/page_view.ex
* creating blog_phoenix/web/router.ex
* creating blog_phoenix/web/web.ex
* creating blog_phoenix/mix.exs
* creating blog_phoenix/README.md
* creating blog_phoenix/lib/blog_phoenix/repo.ex
* creating blog_phoenix/test/support/model_case.ex
* creating blog_phoenix/priv/repo/seeds.exs
* creating blog_phoenix/.gitignore
* creating blog_phoenix/brunch-config.js
* creating blog_phoenix/package.json
* creating blog_phoenix/web/static/css/app.scss
* creating blog_phoenix/web/static/js/app.js
* creating blog_phoenix/web/static/assets/robots.txt
* creating blog_phoenix/web/static/vendor/phoenix.js
* creating blog_phoenix/web/static/assets/images/phoenix.png
* creating blog_phoenix/web/static/assets/images/favicon.ico

Then we need to install dependencies by running:

$ cd blog_phoenix
$ mix deps.get

You can notice that this mix tool is like a hybrid of bundler and rake. Let's check out what Phoenix has generated for us. Go into project directory and run the server:

$ mix phoenix.server

After compiling, we have a running app under http://localhost:4000. Let's see how it looks!

Step 2 — create a table for posts

Now we are ready to start writing our core functionality. We would like to be able to have CRUD actions for posts and also to have the ability to add comments to each post (as it was decided at the beginning - just a simple blog post application). In order to achieve these goals, Phoenix supports us with 4 kinds of generators:

$ mix phoenix.gen.html → which creates: model, view, controllers, repository, templates, tests
$ mix phoenix.gen.channel → which creates: channel and tests
$ mix phoenix.gen.json → for API, which creates: model, view, controllers, repository, tests
$ mix phoenix.gen.model → which creates: model and repository

We use the first generator which creates all resources and actions for us - the same as rails generators. We need to declare the name in singular and plural, and next the field names with types.

$ mix phoenix.gen.html Post posts title:string body:text

Now the CRUD actions for Post are ready.

The following files have been created:

* creating priv/repo/migrations/20150730233126_create_post.exs
* creating web/models/post.ex
* creating test/models/post_test.exs
* creating web/controllers/post_controller.ex
* creating web/templates/post/edit.html.eex
* creating web/templates/post/form.html.eex
* creating web/templates/post/index.html.eex
* creating web/templates/post/new.html.eex
* creating web/templates/post/show.html.eex
* creating web/views/post_view.ex
* creating test/controllers/post_controller_test.exs

Before we refresh the browser, however, we need to add a new endpoint to web/router.ex.

resources "/posts", PostController

To see our routing list we can use:

$ mix phoenix.routes

which is also similar to the one from Rails world. Phoenix uses Ecto by default to communicate and interact with the database. Ecto provides us with adapters to PostgreSQL, MySQL and SQLite (the number of databases supported is still growing). I'm not going too deep, but you can find a good description of Ecto library under Phoenix documentation or on GitHub.

Ecto allows us to create a proper post table in our database by running a migration. To see the application migration files, we need to go to priv/repo/migrations/ and run the migration by command:

$ mix ecto.migrate

but then an error occurs: we didn't create our database, so we have to create a project database using ecto:

$ mix ecto.create
$ mix ecto.migrate

You might notice that this mix ecto.something commands are similar to Rails: rake db:something.

Under http://localhost:4000/posts you can see CRUD functions in action which were generated. Feel free to play around with it.

Step 3 — create a table for comments

Finally, let's write some real code... The next step in our blog application is to enable post comments, namely to obtain the ability to see current post comments and add new ones. Let’s assume that we would like to have many comments for each blog post. Let’s use another generator to create a Comment model and migration by model generator:

$ mix phoenix.gen.model Comment comments name:string content:text post_id:references:posts

For defining associations, as you can see, we use: post:references - the same as in Rails. Remember to add foreign key to Comment model:

defmodule BlogPhoenix.Comment do
  use BlogPhoenix.Web, :model

  schema "comments" do
    field :name, :string
    field :content, :string
    belongs_to :post, BlogPhoenix.Post, foreign_key: :post_id


  @required_fields ~w(name content post_id)
  @optional_fields ~w()

  @doc """
  Creates a changeset based on the `model` and `params`.
  If `params` are nil, an invalid changeset is returned
  with no validation performed.
  def changeset(model, params \\ :empty) do
    |> cast(params, @required_fields, @optional_fields)

The other side of our associations is that our post has many comments so we go into: web/models/post.ex and add: has_many :comments, BlogPhoenix.Comment

defmodule BlogPhoenix.Post do
  use BlogPhoenix.Web, :model

  schema "posts" do
    field :title, :string
    field :body, :string

    has_many :comments, BlogPhoenix.Comment


Model and migration files were created, so we can run migration:

$ mix ecto.migrate

Now that we have a comments table in our database, we need to add the add_comment action to routing and write a proper function in PostControler add_comment/2

resources "/posts", PostController do
  post "/comment", PostController, :add_comment

We have just nested add_comment under /posts, so let’s check out what the routing looks like:

$ mix phoenix.routes

post_post_path  POST    /posts/:post_id/comment  BlogPhoenix.PostController :add_comment

Next, let's make changes to the PostController. We want to have easy access of our Comment model, so we add this alias:

alias BlogPhoenix.Comment

Read more about aliases.

Next add scrub params at the beginning of the controller. Scrub params are similar to strong parameters. Read more about scrub_params

plug :scrub_params, "comment" when action in [:add_comment]

And define add_comment function:

def add_comment(conn, %{"comment" => comment_params, "post_id" => post_id}) do
  changeset = Comment.changeset(%Comment{}, Map.put(comment_params, "post_id", post_id))
  post = Post |> Repo.get(post_id) |> Repo.preload([:comments])

  if changeset.valid? do

    |> put_flash(:info, "Comment added.")
    |> redirect(to: post_path(conn, :show, post))
    render(conn, "show.html", post: post, changeset: changeset)

The changeset function is defined in web/model/comment.ex and it allows us to filter, cast, and validate changes before we apply them to a model. Read more about Ecto changesets.

We have changed the show function preloading post comments and added a Comment changeset because we would like to have a comment form in the post view:

def show(conn, %{"id" => id}) do
  post = Repo.get(Post, id) |> Repo.preload([:comments])
  changeset = Comment.changeset(%Comment{})
  render(conn, "show.html", post: post, changeset: changeset)

We create a comment from template: web/templates/post/comment_form.html.eex

<%= form_for @changeset, @action, fn f -> %>
  <%= if f.errors != [] do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below:</p>
        <%= for {attr, message} <- f.errors do %>
          <li><%= humanize(attr) %> <%= message %></li>
        <% end %>
  <% end %>

  <div class="form-group">
    <%= text_input f, :name, class: "form-control" %>

  <div class="form-group">
    <%= textarea f, :content, class: "form-control" %>

  <div class="form-group">
    <%= submit "Add comment", class: "btn btn-primary" %>
<% end %>

And render that template in post show: web/templates/post/show.html.eex.

<%= render "comment_form.html", post: @post, changeset: @changeset,
action: post_post_path(@conn, :add_comment, @post) %>

Step 4 — display the author and content of a comment

Now we can add new comments to our post, but we still can’t see the results of this action. Since we want to see all comments added to the current post, we need to create a new partial web/templates/post/comments.html.eex and inside it, we iterate through all post comments and display the author of the comment and content. We have already preloaded the comments post in our controller.

<h3> Comments: </h3>
<table class="table">
<%= for comment <- @post.comments do %>
      <td><%= comment.name %></td>
      <td><%= comment.content %></td>
<% end %>

Additionally, we need to render that template in web/templates/post/show.html.eex

<%= render "comments.html", post: @post %>

Step 5 — show number of comments

The last functionality needed is to show the number of comments next to the list of our blog posts. We need to create a query where we can count the number of comments. We can do this inside the model web/models/post.ex by importing Ecto Query module and next we can define the count_comments function which returns a collection of Posts and count as the number of comments.

defmodule BlogPhoenix.Post do
  use BlogPhoenix.Web, :model
  import Ecto.Query


  def count_comments(query) do
    from p in query,
      group_by: p.id,
      left_join: c in assoc(p, :comments),
      select: {p, count(c.id)}

In web/controllers/post_controller.ex inside the index function, we need to use the count_comments function from above:

def index(conn, _params) do
  posts = Post
  |> Post.count_comments
  |> Repo.all
  render(conn, "index.html", posts: posts)

We modified the Posts collection structure a bit, so we need to apply some changes to the template: web/templates/post/index.html.eex:

<h2>Listing posts</h2>
<table class="table">

<%= for {post, count} <- @posts do %>
      <td><%= post.title %></td>
      <td><%= count %></td>

      <td class="text-right">
        <%= link "Show", to: post_path(@conn, :show, post), class: "btn btn-default btn-xs" %>
        <%= link "Edit", to: post_path(@conn, :edit, post), class: "btn btn-default btn-xs" %>
        <%= link "Delete", to: post_path(@conn, :delete, post), method: :delete, class: "btn btn-danger btn-xs" %>
<% end %>

Let's see how our blog post application works: http://localhost:4000/posts


The source code for this application is stored on GitHub. This Blog application is very simple and it's just an attempt to prove how easy it is to play with Elixir and the Phoenix framework when we have a Ruby and Rails background. As you can see, it's as easy and fun as it was with Rails! I can remember I felt the same enthusiasm when I first met Ruby :) Is this, perhaps, something you call love at first sight?

We can see many similarities between Elixir on Phoenix and Ruby on Rails as far as conventions are concerned. This framework offers many familiar concepts like models, routing, controllers, and form helpers, but also some new approaches like repositories, changesets and channels. Because of that, we feel much more comfortable writing the code, but we have to remember that it's not an Object-oriented style of programming anymore. Therefore, we need to set our minds to ‘functional programming mode’ - and it’s equally as exciting!

I hope this tutorial encourages you to take a closer look into Elixir and Phoenix on your own. Who knows, maybe Elixir on Phoenix becomes the next generation web standard?

Looking for Elixir developers?

Our devs are so communicative and diligent you’ll feel they are your in-house team. Work with JavaScript experts who will push hard to understand your business and squeeze the most out of Elixir.

Talk to our team and confidently build your next big thing.

Jakub Cieślar avatar
Jakub Cieślar