Web Development

First steps in Elixir and Phoenix - Create a blog prototype

Vlad Craciun

Vlad Craciun

We always have a hands on approach when learning new stuff and this article will help you learn the basics of Elixir and the Phoenix Framework by building a basic blog.

We'll asume you already have Elixir and Phoenix up and running.

Our purpose here is to create a simple CRUD app which will allow us to use basic data operations like save, edit and delete. Let's start by creating a simple blog with posts and comments.

#1 Setup our Elixir project

To start a project in Elixir, we use Mix, a build tool which provides everything necessary to create and manage Elixir apps. First, we need to use a mix task to create our project:

Create a new elixir project (1)

    
      
              $ mix phx.new blog_app
              
    
  

The mix task creates our new blog_app template and files. You should see something like this:

Create a new elixir project (2)

    
      
              * creating blog_app/config/config.exs * creating blog_app/config/dev.exs
              * creating blog_app/config/prod.exs * creating blog_app/config/prod.secret.exs
              * creating blog_app/config/test.exs
              * creating blog_app/lib/blog_app/application.ex * creating blog_app/lib/blog_app.ex
              ...
              * creating blog_app/assets/css/phoenix.css
              * creating blog_app/assets/static/images/phoenix.png
              * creating blog_app/assets/static/robots.txt

              Fetch and install dependencies? [Yn] y

              * running mix deps.get
              * running mix deps.compile
              * running cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development
              
    
  

We're almost there! Let's move into the project's folder:

Create a new elixir project [3]

    
      
              $ cd blog_app
              
    
  

Then configure your database in config/dev.exs and run:

Create a new elixir project (4)

    
      
              $ mix ecto.create
              
    
  

Finally, start your Phoenix app with:

Create a new elixir project (5)

    
      
              $ mix phx.server
              
    
  

You can also run your app inside IEx (Interactive Elixir) as:

Create a new elixir project (6)

    
      
              $ iex -S mix phx.server
              
    
  

If you chose "No" when asked to install dependencies, you can do it afterwards with the following mix command:

Install dependencies

    
      
              $ mix deps.get
              
    
  

For the next step open up config/dev.exs in a text editor and configure the database. Make sure that the username and password match the ones you have set up on your local machine. It should look like this:

Database configuration

    
      
              config :blog_app, BlogApp.Repo,
                username: "postgres",
                password: "postgres",
                database: "blog_app_dev",
                hostname: "localhost",
                show_sensitive_data_on_connection_error: true,
                pool_size: 10
              
    
  

As prompted earlier, we then need to cd into our newly created project and run the $ mix ecto.create task to create the database:

Create database

    
      
              $ cd blog_app
              $ blog_app mix ecto.create

              Compiling 14 files (.ex) Generated blog_app app The database for BlogApp.Repo has been created
              
    
  

Let's see if everything works as it's supposed to. We'll start up the server by running either mix phx.server or iex -S mix phx.server (IEx stands for Interactive Elixir). Our application will be running under http://localhost:4000.

We should be presented with the "Welcome to Phoenix!" page:

'Welcome to Phoenix!' page
"Welcome to Phoenix!" page

#2 Create a Post resource

Phoenix provides an easy way to setup the basic resources we need through the use of generators - mix tasks which will build the respective modules. What we need is the phx.gen.html, which will generate the controller, views and context.

Fire up the terminal and enter this command:

Generate context, schema and database table

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

What we did here, was creating the Post resource. The resource belongs to the Posts context and has a posts schema, with a title field of type string and a body field of type text.

The CRUD actions for our Post resource are now ready. The following files have been created:

Creating controller, views and context files

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

              * creating lib/blog_app_web/controllers/post_controller.ex
              * creating lib/blog_app_web/templates/post/edit.html.eex
              * creating lib/blog_app_web/templates/post/form.html.eex
              ...
              * creating lib/blog_app/posts.ex
              * injecting lib/blog_app/posts.ex
              * creating test/blog_app/posts_test.exs
              * injecting test/blog_app/posts_test.exs Add the resource to your browser scope in lib/blog_app_web/router.ex:

              resources "/posts", PostController

              Remember to update your repository by running migrations:
              $ mix ecto.migrate
              
    
  

If we take a look in the lib/blog_app/posts/post.ex file of our blog_app, we'll see the posts schema matching the database table 'posts' we created.

lib/blog_app/posts/post.ex

    
      
              defmodule BlogApp.Posts.Post do
                use Ecto.Schema
                import Ecto.Changeset

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

                  timestamps()
                end

                @doc false
                def changeset(post, attrs) do
                  post
                  |> cast(attrs, [:title, :body])
                  |> validate_required([:title, :body])
                end
              end
              
    
  

In lib/blog_app_web/controllers/post_controller.ex - you'll notice that we can access the Post resource via the Posts context we created.

lib/blog_app_web/controllers/post_controller.ex

    
      
              defmodule BlogAppWeb.PostController do
                use BlogAppWeb, :controller

                alias BlogApp.Posts
                alias BlogApp.Posts.Post

                def index(conn, _params) do
                  posts = Posts.list_posts()
                  render(conn, "index.html", posts: posts)
                end

                def new(conn, _params) do
                  changeset = Posts.change_post(%Post{})
                  render(conn, "new.html", changeset: changeset)
                end

                ...
              end
              
    
  

In the lib/blog_app/posts.ex we can see several functions that Phoenix implements by default.

lib/blog_app/posts.ex

    
      
              def list_posts do
                Repo.all(Post)
              end

              def get_post!(id), do: Repo.get!(Post, id)

              def create_post(attrs \\ %{}) do
                %Post{}
                |> Post.changeset(attrs)
                |> Repo.insert()
              end

              def update_post(%Post{} = post, attrs) do
                post
                |> Post.changeset(attrs)
                |> Repo.update()
              end

              def delete_post(%Post{} = post) do
                Repo.delete(post)
              end

              def change_post(%Post{} = post, attrs \\ %{}) do
                Post.changeset(post, attrs)
              end
              
    
  

Now let's open the lib/blog_app_web/router.ex module and define the route for our new posts resource.

Under the get "/", PageController, :index line, add resources "/posts", PostController.

lib/blog_app_web/router.ex

    
      
              scope "/", BlogAppWeb do
                pipe_through :browser

                get "/", PageController, :index
                resources "/posts", PostController
              end
              
    
  

This has generated a new database migration, therefore we need to run the task mix ecto.migrate to persist the changes.

Now let's run our server again with iex -S mix phx.server and go to http://localhost:4000/posts. We'll see the Listing Posts page. Notice that we can do all the basic CRUD operations with posts.

Creating blog posts in elixir and phoenix
CRUD operations for posts
Creating blog posts in elixir and phoenix
CRUD operations for posts

Using mix phx.routes we can inspect all the existing routes for our app. We can combine it with grep, mix phx.routes | grep posts to inspect only the posts routes.

Inspecting post routes

    
      
              $ mix phx.routes | grep posts

              post_path GET /posts BlogAppWeb.PostController :index
              post_path GET /posts/:id/edit BlogAppWeb.PostController :edit
              post_path GET /posts/new BlogAppWeb.PostController :new
              post_path GET /posts/:id BlogAppWeb.PostController :show
              post_path POST /posts BlogAppWeb.PostController :create
              post_path PATCH /posts/:id BlogAppWeb.PostController :update
              PUT /posts/:id BlogAppWeb.PostController :update
              post_path DELETE /posts/:id BlogAppWeb.PostController :delete
              
    
  

#3 Add Comments to our Posts

Let's enable comments for our posts by using another Phoenix generator, phx.gen.context. This will create another context and the comments ecto schema:

Generating the comments context and schema

    
      
              $ mix phx.gen.context Comments Comment comments name:string content:text post_id:references:posts
              
    
  

The post_id:references:posts is the way you tell the generator to setup a relationship between Post and Comment. We'll see the post_id field added to the comments schema.

We're not finished yet, as we still need to define the association between the posts and the comments schemas ourselves.

We'll make use of the Ecto.Schema function has_many for the post.

lib/blog_app/posts/post.ex

    
      
              defmodule BlogApp.Posts.Post do
                use Ecto.Schema
                import Ecto.Changeset
                alias BlogApp.Comments.Comment

                schema "posts" do
                  field :body, :string
                  field :title, :string
                  has_many :comments, Comment

                  timestamps()
                end

                @doc false
                def changeset(post, attrs) do
                  post
                  |> cast(attrs, [:title, :body])
                  |> validate_required([:title, :body])
                end
              end
              
    
  

Here we used the alias directive to reference the Comment like this: has_many :comments, Comment.

In the comment schema, we already have the post_id field added by the mix task, so we only need to add the post_id to the changeset below.

lib/blog_app/comments/comment.ex

    
      
                defmodule BlogApp.Comments.Comment do
                  use Ecto.Schema
                  import Ecto.Changeset

                  schema "comments" do
                    field :content, :string
                    field :name, :string
                    field :post_id, :id

                    timestamps()
                  end

                  @doc false
                  def changeset(comment, attrs) do
                    comment
                    |> cast(attrs, [:name, :content, :post_id])
                    |> validate_required([:name, :content, :post_id])
                  end
                end
              
    
  

Now we need to run the mix ecto.migrate task again.

After we're done with associations, we need to make it possible for the user to see and interact with post comments from the app interface.

In the router module we need to add another resource for the comments, with the add_comment action and create its corresponding function in PostControler.

router.ex

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

In the post_controller.ex file we'll add the function:

This will allow the creation of a new comment in the database and associate it to a post. It will also load the post page and show a flash message with the status of the create operation.

lib/blog_app_web/controllers/post_controller.ex

    
      
              def add_comment(conn, %{"comment" => comment_params, "post_id" => post_id}) do
                post =
                  post_id
                  |> Posts.get_post!()
                  |> Repo.preload([:comments])
                case Posts.add_comment(post_id, comment_params) do
                  {:ok, _comment} ->
                    conn
                    |> put_flash(:info, "Added comment!")
                    |> redirect(to: Routes.post_path(conn, :show, post))
                  {:error, _error} ->
                    conn
                    |> put_flash(:error, "Oops! Couldn't add comment!")
                    |> redirect(to: Routes.post_path(conn, :show, post))
                end
              end
              
    
  

Make sure to alias the BlogApp.Repo at the beginning of the post_controller file.

Now into the /lib/blog_app/posts.ex module we'll add the add_comment action. Also remember to alias BlogApp.Comments.

/lib/blog_app/posts.ex

    
      
                def add_comment(post_id, comment_params) do
                  comment_params
                  |> Map.put("post_id", post_id)
                  |> Comments.create_comment()
                end
              
    
  

We'll also need to create a simple form on the post's page in order to allow users to comment. To achieve this, we'll update the show action on the post controller. We need to preload comments from the Repo and include a Comment.changeset into its show.html template, and alias BlogApp.Comments.Comment to be able to use the Comment.changeset.

lib/blog_app_web/controllers/post_controller.ex

    
      
                def show(conn, %{"id" => id}) do
                  post =
                    id
                    |> Posts.get_post!
                    |> Repo.preload([:comments])

                  changeset = Comment.changeset(%Comment{}, %{})
                  render(conn, "show.html", post: post, changeset: changeset)
                end
              
    
  

After that's done, we'll create a comment_form.html.eex template:

lib/blog_app_web/templates/post/comment_form.html.eex

    
      
              <%= form_for @changeset, @action, fn f -> %>
                <div class="form-group">
                  <label>Name</label>
                  <%= text_input f, :name, class: "form-control" %>
                </div>
                <div class="form-group">
                  <label>Content</label>
                  <%= textarea f, :content, class:"form-control" %>
                </div>
                <div class="form-group">
                  <%= submit "Add comment", class:"btn btn-primary" %>
                </div>
              <% end %>
              
    
  

Now let's get to the lib/blog_app_web/templates/post/show.html.eex template and add in this line above the edit and back links:

lib/blog_app_web/templates/post/show.html.eex

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

This will render our comment form on the post's page.

Form for adding comments to a post
Add Comment form

We can now add comments to our posts, but we can't see them yet. Let's display them in the blog pages.

#4 Displaying Post Comments

We'll need to create yet another template for our posts. Let's call it comments.html.eex.

lib/blog_app_web/templates/post/comments.html.eex

    
      
              <h3>Comments:</h3>
              <div class="comments">
                <div class="comment header">
                  <div>Name</div>
                  <div>Content</div>
                </div>
                <%= for comment <- @comments do %>
                <div class="comment">
                  <div><%= comment.name %></div>
                  <div><%= comment.content%></div>
                </div>
                <% end %>
              </div>
              
    
  

Then we'll render this new template on our posts page next to the comment_form.

lib/blog_app_web/templates/post/show.html.eex

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

Add a little bit of styling in app.scss:

assets/css/app.scss

    
      
              .comments {
                padding-bottom: 2em;
              }
              .comment {
                display: grid;
                grid-template-columns: 1fr 1fr;
                padding: 0.5em;
                border-bottom: 1px solid lightgrey;
              }
              .comment.header {
                font-weight: bold;
              }
              
    
  

The comments are now displayed:

Styled comments on post page

What if we would like to see the total number of comments for a post? Let's do that too:

We need to define a function to the get the number of posts in the BlogApp.Posts context. So let's get back to the lib/blog_app/posts.ex and add this function after add_comment:

lib/blog_app/posts.ex

    
      
              def get_number_of_comments(post_id) do
                post = Posts.get_post!(post_id) |> Repo.preload([:comments])
                Enum.count(post.comments)
              end
              
    
  

Now, let's update BlogApp.PostView module in lib/blog_app_web/views/post_view.ex like so:

lib/blog_app_web/views/post_view.ex

    
      
              defmodule BlogWeb.PostView do
                use BlogWeb, :view
                alias BlogApp.Posts

                def get_comments_count(post_id) do
                  Posts.get_number_of_comments(post_id)
                end
              end
              
    
  

Finally, in the index.html.eex posts file we can add these lines to update our post. <th>Comments</th> in the table head, and <td><%= get_comments_count(post.id) %></td> in the corresponding table body.

Here's how our blog looks now:

Posts page with comment number
Final posts page with comments number

Hurray! We've got our basic blog up and running!

Make sure you check out the official Elixir and Phoenix documentation, wich covers more advanced topics.

If we missed something, let's start a conversation on Twitter.

We partner closely with our clients

To create and support the strategic IT vision for their business.