DEV Community

Jonathan Yeong
Jonathan Yeong

Posted on • Originally published at jonathanyeong.com on

Building a blog with Phoenix: Getting started

Today, I want to show you how to start building a blog using Phoenix. You can find the source code here: https://github.com/jonathanyeong/phoenix_blog/tree/v0.0.1. We are using Phoenix version 1.5.3 and Elixir version 1.10.3. We will cover:


Project Setup

After installing Phoenix and Elixir (see Phoenix docs). Setup your new blog application.

mix phx.new phoenix_blog
cd phoenix_blog/
mix ecto.create

Enter fullscreen mode Exit fullscreen mode

Generate migration

Firstly, we need to create the Post table. We can use the mix task phx.gen.schema to generate a schema and a migration file (see the docs for more info). The schema is how we represent information from the database within our application. While, the migration contains the commands that run on the database itself.

mix phx.gen.schema Post posts content:text title:string
mix ecto.migrate

Enter fullscreen mode Exit fullscreen mode

Add Routes

Now that we’ve run the migration, we need to add routes to our app so that we can view and create posts on the front-end. We will be using resources to generate resourceful routes for us.

In lib/phoenix_blog_web/router.ex

scope "/", PhoenixBlogWeb do
  resources "/posts", PostController
end

Enter fullscreen mode Exit fullscreen mode

If you run mix phx.routes. You should see:

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

Enter fullscreen mode Exit fullscreen mode

Now if you start the Phoenix server

mix phx.server

Enter fullscreen mode Exit fullscreen mode

And navigate to localhost:4000/posts you will get an error. Let’s fix this.

Add the Post Controller

All of these routes point to a nonexistent Post Controller. Lets create one and add the following code.

# lib/phoenix_blog_web/controllers/post_controller.ex
defmodule PhoenixBlogWeb.PostController do
  use PhoenixBlogWeb, :controller

  alias PhoenixBlog.{
    Post,
    Repo
  }

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

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

  def create(conn, %{"post" => post_params} = _params) do
    changeset = Post.changeset(%Post{}, post_params)
    case Repo.insert(changeset) do
      {:ok, _log} ->
        conn
        |> put_flash(:info, "Success - created a Post!")
        |> redirect(to: Routes.post_path(conn, :index))
      {:error, changeset} ->
        conn
        |> put_flash(:error, "failure couldn't create post")
        |> render(:new, changeset: changeset)
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

Jump to Post View & Templates.

Breakdown

defmodule PhoenixBlogWeb.PostController do
  ...
end

Enter fullscreen mode Exit fullscreen mode

Controllers are just modules in Elixir. The module name (PostController) needs to match the filename post_controller.ex.

use PhoenixBlogWeb, :controller

Enter fullscreen mode Exit fullscreen mode

use will require and inject the Phoenix.Controller code and the Plug.Conn into our Post Controller. It does this by calling the macro __using()__ . You can see the code injected here.

alias PhoenixBlog.{
  Post,
  Repo
}

Enter fullscreen mode Exit fullscreen mode

Alias is used to define shortcuts. Instead of writing PhoenixBlog.Post we can instead write Post.

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

Enter fullscreen mode Exit fullscreen mode

The index method takes in a conn and a params argument. conn comes from Plug.Conn. Which was injected above via use. conn is a module that defines low level functions such as request information. Next is params, we will prepend it with _ to signify that we aren’t using it. We then call Repo.all(Post) to pull all the posts from the database. These posts are assigned to a variable which we can access in the index view via @posts.

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

Enter fullscreen mode Exit fullscreen mode

The new method creates a changeset that is passed to the new view. A changeset in Elixir will only allow data with specific information to go through. It will also complain if it expects data but doesn’t receive it. Post.changeset/2 method takes in a Post struct and some attributes. Since we’re creating a new Post, there are no attributes so instead we’ll pass in an empty map.

def create(conn, %{"post" => post_params} = _params) do
  changeset = Post.changeset(%Post{}, post_params)
  case Repo.insert(changeset) do
    {:ok, _log} ->
      conn
      |> put_flash(:info, "Success - created a Post!")
      |> redirect(to: Routes.post_path(conn, :index))
    {:error, changeset} ->
      conn
      |> put_flash(:error, "failure couldn't create post")
      |> render(:new, changeset: changeset)
  end
end

Enter fullscreen mode Exit fullscreen mode

The create method is pretty big so let’s break this down to smaller parts.

...%{"post" => post_params} = _params

Enter fullscreen mode Exit fullscreen mode

The first line of the method uses Elixir’s pattern matching to assign anything found in params["post"] to post_params.

changeset = Post.changeset(%Post{}, post_params)

Enter fullscreen mode Exit fullscreen mode

We’ve seen changeset being used for the new route. In the create route we’re passing in the post_params instead of an empty map. This creates a changeset that is populated by the form data. Which we’ll save to the database via:

Repo.insert(changeset)

Enter fullscreen mode Exit fullscreen mode

Repo.insert/1 will return one of two tuples.

{:ok, _log} ->
  conn
  |> put_flash(:info, "Success - created a Post!")
  |> redirect(to: Routes.post_path(conn, :index))

Enter fullscreen mode Exit fullscreen mode

On a successful insert into the database, it will display a success flash message and redirect to the index page where all the posts will be displayed.

{:error, changeset} ->
  conn
  |> put_flash(:error, "failure couldn't create post")
  |> render(:new, changeset: changeset)

Enter fullscreen mode Exit fullscreen mode

On an error inserting into the database, it will display an error flash message and re-render new.html.eex.

Add Post View & Templates

Phoenix’s has a strong naming convention, which will link templates in the post directory and the view, post_view, to the Post Controller.

Templates in Phoenix are written in eex which stands for Embedded Elixir. They are the building blocks to a webpage. When we start the Phoenix server, templates get rendered into the view and displayed as html.

# lib/phoenix_blog_web/views/post_view.ex
defmodule PhoenixBlogWeb.PostView do
  use PhoenixBlogWeb, :view
end

# lib/phoenix_blog_web/templates/index.html.eex.
<h1>Blog Post</h1>
<%= link "+ Add new post", to: Routes.post_path(@conn, :new) %>
<%= for post <- @posts do %>
<h2><%= post.title %></h2>
<p><%= post.content%></p>
<% end %>

# lib/phoenix_blog_web/templates/post/new.html.eex
<h1>New Blog Post</h1>

<%= form_for @changeset, Routes.post_path(@conn, :create), fn f -> %>
  <label>
    title:
  </label>
  <%= text_input f, :title %>

  <label>
    Content:
  </label>
  <%= textarea f, :content %>

  <%= submit "Submit!" %>
<% end %>

Enter fullscreen mode Exit fullscreen mode

Breakdown

# lib/phoenix_blog_web/views/post_view.ex
defmodule PhoenixBlogWeb.PostView do
  use PhoenixBlogWeb, :view
end

Enter fullscreen mode Exit fullscreen mode

The Post view has a similar structure to the Post controller. use will inject the Phoenix.View code.

# lib/phoenix_blog_web/templates/index.html.eex.
<h1>Blog Post</h1>
<%= link "+ Add new post", to: Routes.post_path(@conn, :new) %>
<%= for post <- @posts do %>
<h2><%= post.title %></h2>
<p><%= post.content%></p>
<% end %>

Enter fullscreen mode Exit fullscreen mode

The index view will add a link that routes to the new Post path. It will also loop through all the posts and display the title and content. The @posts variable was set in the PostController index method.

# lib/phoenix_blog_web/templates/post/new.html.eex
<h1>New Blog Post</h1>

<%= form_for @changeset, Routes.post_path(@conn, :create), fn f -> %>
  <label>
    title:
  </label>
  <%= text_input f, :title %>

  <label>
    Content:
  </label>
  <%= textarea f, :content %>

  <%= submit "Submit!" %>
<% end %>

Enter fullscreen mode Exit fullscreen mode

The form inside new.html will use the changeset variable we set in the PostController new method. If you want more information on the types of input fields, see the Phoenix docs. When the user clicks on submit, the form data gets sent to the PostController create method via Routes.post_path(@conn, :create).

Conclusion

Phew, we finally made it! Now if you start your webserver:

mix phx.server

Enter fullscreen mode Exit fullscreen mode

Navigate to localhost:4000/posts you should see Blog Post as a header. If you click on the link or go to localhost:4000/posts/new you should see a form to create a post.

Try implementing the update and edit route next! You can apply the patterns in new and create to help guide you. I hope this tutorial has highlighted how easy it is to get started with Phoenix. If you enjoyed this article please leave a comment below. Happy coding!

Top comments (2)

Collapse
 
cescquintero profile image
Francisco Quintero 🇨🇴

Awesome. I really like how it feels so close to Ruby on Rails.

Collapse
 
jonathanyeong profile image
Jonathan Yeong

Me too! It's definitely made the learning curve easier. I do think Phoenix feels snappier than Rails.