Phoenix’s Basics

Phoenix follows the Model-View-Controller (MVC) architecture. If you are not familiar with that: Just follow my lead. MVC is just a set of rules of what goes where so that everybody knows where stuff is. No one expects you to follow it, but it makes life easier if you do. After a couple of examples, you’ll get a feeling for it. Same goes for the directory structure and the file names.

In this chapter, I’ll show you how you can create simple webpages which have some programming logic. But we will not touch the database yet. Step by step!

Phoenix version

The code examples of this book are written and tested for Phoenix version 1.5.1 or above. Please make sure that you have a 1.5.x version installed.

$ mix phx.new --version
Phoenix v1.5.1
In some old webpages you’ll find mix phoenix.new instead of mix phx.new. That is outdated. Since Phoenix version 1.3 the syntax is mix phx.whatever.

Development Environment

By default, Phoenix offers three different environments:

  • Development

  • Testing

  • Production

In this chapter, we are only going to use the development environment. It offers some pleasantries which make the life of a developer easier (e.g. more verbose error messages and auto-reload after code changes).

The Base Setup

The mix phx.new application_name command creates the foundation of every Phoenix application which generates all needed files and the directory structure.

We call our new application demo:

$ mix phx.new demo --no-ecto (1)
* creating demo/config/config.exs
* creating demo/config/dev.exs
[...]
Fetch and install dependencies? [Yn] Y (2)
[...]
We are almost there! The following steps are missing:

    $ cd demo

Start your Phoenix app with:

    $ mix phx.server

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

    $ iex -S mix phx.server
1 '--no-ecto' creates a new application without the Ecto (database connector) part. We don’t need a database for the basic examples.
2 You always want to press Y here. And yes, it sometimes takes forever.

After that, we cd demo into the new directory and fire up the Phoenix server with mix phx.server

$ cd demo
$ mix phx.server
Compiling 13 files (.ex) (1)
Generated demo app
[info] Running DemoWeb.Endpoint with cowboy 2.7.0 at 0.0.0.0:4000 (http)
[info] Access DemoWeb.Endpoint at http://localhost:4000 (2)

webpack is watching the files…

Hash: f3ee21a2f5780f52f176
Version: webpack 4.41.5
[...]
1 Source-Code which hasn’t been compiled yet is compiled.
2 The URL which serves the development website: http://localhost:4000

Please open the URL http://localhost:4000 in your web browser:

http://localhost:4000

On the terminal you can see the server log:

[info] GET /
[debug] Processing with DemoWeb.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 22ms

Do a reload in the browser and watch the log output:

[info] GET /
[debug] Processing with DemoWeb.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 426µs
The log says that the request was answered in 426µs. WOW! Microseconds! That is on my old development laptop. That must be the great speed of Phoenix/Elixir everybody is talking about.

What we see is the default index.html.eex page. The file extension eex stands for Embedded Elixir and means that this is a mix of static HTML and dynamic Elixir code which generated HTML. These files are called templates and are located in the lib/demo_web/templates/ directory. A new Phoenix application has two templates:

$ tree lib/demo_web/templates/
lib/demo_web/templates/
├── layout
│   └── app.html.eex
└── page
    └── index.html.eex
It is no coincidence that the subdirectory is called demo_web. Because we named our application demo that name will be used in multiple places.

Let’s have a look into index.html.eex which contains the core part of the page.

lib/demo_web/templates/page/index.html.eex
<section class="phx-hero">
  <h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
  <p>Peace-of-mind from prototype to production</p>
</section>

<section class="row">
  <article class="column">
    <h2>Resources</h2>
    <ul>
      <li>
        <a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
      </li>
      <li>
        <a href="https://github.com/phoenixframework/phoenix">Source</a>
      </li>
      <li>
        <a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a>
      </li>
    </ul>
  </article>
  <article class="column">
    <h2>Help</h2>
    <ul>
      <li>
        <a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
      </li>
      <li>
        <a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
      </li>
      <li>
        <a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
      </li>
      <li>
        <a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
      </li>
    </ul>
  </article>
</section>

But a bit of HTML boilerplate is missing and can be found in lib/demo_web/templates/layout/app.html.eex.

lib/demo_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en"> (1)
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Demo · Phoenix Framework</title> (2)
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/> (3)
    <script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </head>
  <body>
    <header> (4)
      <section class="container">
        <nav role="navigation">
          <ul>
            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
            <%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
              <li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
            <% end %>
          </ul>
        </nav>
        <a href="https://phoenixframework.org/" class="phx-logo">
          <img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
        </a>
      </section>
    </header>
    <main role="main" class="container">
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> (5)
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <%= @inner_content %> (6)
    </main>
  </body>
</html>
1 You might want to change the language here in case this webpage is going to be in an other language than English.
2 You probably want to change this to a better <title>.
3 Phoenix’s asset management takes care of the CSS and JavaScript. No need to worry about it for now.
4 This is the boilerplate header part you are seeing on the top of every page.
5 This part renders so called flash messages. We’ll get to that later.
6 This is the line where the template’s content gets included.
Embedded Elixir (.eex) uses the <% %> syntax to embed Elixir code in HTML. <% %> runs the Elixir code within. <%= %> runs the Elixir code and includes the result of that as HTML in the template.

Feel free to change the content of app.html.eex and index.html.eex while having http://localhost:4000 opened in a browser. In development mode, each save of those files triggers a reload of the page in the browser.

Hello World!

This section aims to create a new dynamic page which is available at http://localhost:4000/hello and displays the text "Hello World!". We start with the base setup:

$ mix phx.new demo --no-ecto
[...]
$ cd demo
$ mix phx.server

Routes are defined in lib/demo_web/router.ex. Let’s have a look and add a new route for our hello world page.

lib/demo_web/router.ex
defmodule DemoWeb.Router do
  use DemoWeb, :router

  [...]

  scope "/", DemoWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/hello", PageController, :hello (1)
  end

  [...]
1 We use the same PageController as the :index action for our new :hello action (function).

Because the route calls the :hello action in the PageController we have to add a hello/2 function in page_controller.ex:

lib/demo_web/controllers/page_controller.ex
defmodule DemoWeb.PageController do
  use DemoWeb, :controller

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

  def hello(conn, _params) do (1)
    render(conn, "hello.html")
  end
end
1 The new hello/2 function renders the hello.html template.

Last step: We have to create a template file. Please do so and include this source code into it:

lib/demo_web/templates/page/hello.html.eex
<h1>Hello World!</h1>

Now open http://localhost:4000/hello in your browser:

http://localhost:4000/hello

Hello World with its controller

In the last section, we added the :hello action to the already existing PageController. But in many cases, it makes sense to create a separate controller. Let’s do that, so you know how to.

We start with changing the route:

lib/demo_web/router.ex
defmodule DemoWeb.Router do
  use DemoWeb, :router

  [...]

  scope "/", DemoWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/hello", ExampleController, :hello (1)
  end

  [...]
1 Yes, ExampleController is not a candidate for best controller name of the year. Good catch!

Let’s be lazy and ask Phoenix what to do next. We open http://localhost:4000/hello in the browser:

http://localhost:4000/hello

It says function DemoWeb.ExampleController.init/1 is undefined which leads us to the next missing piece: A controller. That file needs to be named example_controller.ex and is has to be saved in the lib/demo_web/controllers directory. Here is the content of it:

lib/demo_web/controllers/example_controller.ex
defmodule DemoWeb.ExampleController do (1)
  use DemoWeb, :controller

  def hello(conn, _params) do
    render(conn, "hello.html")
  end
end
1 Important: DemoWeb.ExampleController

After a reload we get a new error message: function DemoWeb.ExampleView.render/2 is undefined. So we need to create a view file:

lib/demo_web/views/example_view.ex
defmodule DemoWeb.ExampleView do (1)
  use DemoWeb, :view
end
1 Important to use the right name here (e.g. 'ExampleView').

A reload, and we get our final error message:

http://localhost:4000/hello

The template is missing. But that is an easy fix:

lib/demo_web/templates/example/hello.html.eex
<h1>Hello World!</h1>

And here is our good to go webpage:

http://localhost:4000/hello

Checklist for a new page

Every time you want to create a new action in a new controller, you have to take care of these steps:

  • Create a route in lib/demo_web/router.ex

  • Create a controller with the name lib/demo_web/controllers/example_controller.ex

  • Create an action in that controller which matches the route

  • Create a view with the name lib/demo_web/views/example_view.ex

  • Create a template with the name lib/demo_web/templates/page/hello.html.eex

Phoenix will always lead you through the way. If something is missing it will say so in the error message.

Obviously demo_web, example_controller.ex, example_view.ex and hello.html.eex are just names which fit for our "Hello World!" example. You have to adjust them for your case.

For our example the directory and file structure looks like this:

$ tree lib/demo_web/{cont*,temp*,view*}
lib/demo_web/controllers
├── example_controller.ex
└── page_controller.ex
lib/demo_web/templates
├── example
│   └── hello.html.eex
├── layout
│   └── app.html.eex
└── page
    └── index.html.eex
lib/demo_web/views
├── error_helpers.ex
├── error_view.ex
├── example_view.ex
├── layout_view.ex
└── page_view.ex

The conn Struct

According to the Model-View-Controller (MVC) architecture we do our programming stuff in the controller and use the template just to display the results. Therefore we need a mechanism to transport this data from the controller into the template. That mechanism is the conn struct. Let’s have a look into it:

$ mix phx.new demo --no-ecto
[...]
$ cd demo
$ mix phx.server

A new route to inspect the content of conn and we add a second route for a playground page:

lib/demo_web/router.ex
defmodule DemoWeb.Router do
  use DemoWeb, :router

  [...]

  scope "/", DemoWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/inspect", PageController, :inspect (1)
    get "/playground", PageController, :playground
  end

  [...]
1 For now we put it into the PageController.

In the page controller we add an inspect and a playground action:

lib/demo_web/controllers/page_controller.ex
defmodule DemoWeb.PageController do
  use DemoWeb, :controller

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

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

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

And finally this piece of code into the inspect.html.eex template:

lib/demo_web/templates/page/inspect.html.eex
<pre>
<%= inspect(@conn, pretty: true) %> (1)
</pre>
1 We have access to conn in the template by calling it @conn.

Please open http://localhost:4000/inspect in your browser:

http://localhost:4000/inspect

That is quite a bit of information in the @conn struct. Here is the complete content:

%Plug.Conn{
  adapter: {Plug.Cowboy.Conn, :...},
  assigns: %{layout: {DemoWeb.LayoutView, "app.html"}},
  before_send: [#Function<0.39862366/1 in Plug.CSRFProtection.call/2>,
   #Function<2.67121911/1 in Phoenix.Controller.fetch_flash/2>,
   #Function<0.29283909/1 in Plug.Session.before_send/2>,
   #Function<0.24098476/1 in Plug.Telemetry.call/2>,
   #Function<0.67312369/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>],
  body_params: %{},
  cookies: %{},
  halted: false,
  host: "localhost",
  method: "GET",
  owner: #PID<0.855.0>,
  params: %{},
  path_info: ["inspect"],
  path_params: %{},
  port: 4000,
  private: %{
    DemoWeb.Router => {[], %{}},
    :phoenix_action => :inspect,
    :phoenix_controller => DemoWeb.PageController,
    :phoenix_endpoint => DemoWeb.Endpoint,
    :phoenix_flash => %{},
    :phoenix_format => "html",
    :phoenix_layout => {DemoWeb.LayoutView, :app},
    :phoenix_request_logger => {"request_logger", "request_logger"},
    :phoenix_router => DemoWeb.Router,
    :phoenix_template => "inspect.html",
    :phoenix_view => DemoWeb.PageView,
    :plug_session => %{},
    :plug_session_fetch => :done
  },
  query_params: %{},
  query_string: "",
  remote_ip: {127, 0, 0, 1},
  req_cookies: %{},
  req_headers: [
    {"accept",
     "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"},
    {"accept-encoding", "gzip, deflate, br"},
    {"accept-language", "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"},
    {"connection", "keep-alive"},
    {"host", "localhost:4000"},
    {"sec-fetch-dest", "document"},
    {"sec-fetch-mode", "navigate"},
    {"sec-fetch-site", "none"},
    {"sec-fetch-user", "?1"},
    {"upgrade-insecure-requests", "1"},
    {"user-agent",
     "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"}
  ],
  request_path: "/inspect",
  resp_body: nil,
  resp_cookies: %{},
  resp_headers: [
    {"cache-control", "max-age=0, private, must-revalidate"},
    {"x-request-id", "FhBrYjjxnpjbwzAAAAxD"},
    {"x-frame-options", "SAMEORIGIN"},
    {"x-xss-protection", "1; mode=block"},
    {"x-content-type-options", "nosniff"},
    {"x-download-options", "noopen"},
    {"x-permitted-cross-domain-policies", "none"},
    {"cross-origin-window-policy", "deny"}
  ],
  scheme: :http,
  script_name: [],
  secret_key_base: :...,
  state: :unset,
  status: nil
}

We can use the playground.html.eex to display specific parts of that:

lib/demo_web/templates/page/playground.html.eex
<table>
  <tr><td>Host:</td><td><%= @conn.host %></td></tr>
  <tr><td>Port:</td><td><%= @conn.port %></td></tr>
</table>

Please open http://localhost:4000/playground to see the result.

http://localhost:4000/playground

Let me show you now how to use conn to transport additional data:

lib/demo_web/controllers/page_controller.ex
defmodule DemoWeb.PageController do
  use DemoWeb, :controller

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

  def inspect(conn, _params) do
    conn
    |> assign(:headline, "This is a test headline") (1)
    |> render("inspect.html")
  end

  def playground(conn, _params) do
    headline = "This is a test headline"

    conn
    |> assign(:headline, headline) (2)
    |> render("playground.html")
  end
end
1 With assign/3 we can add new variables to the conn struct.
2 Same result but a different coding style.
%Plug.Conn{
  adapter: {Plug.Cowboy.Conn, :...},
  assigns: %{
    headline: "This is a test headline",
    layout: {DemoWeb.LayoutView, "app.html"}
  },
[...]

To access that we change the playground.html.eex template:

lib/hello_world_web/templates/page/playground.html.eex
<h1><%= @headline %></h1>

<table>
  <tr>
    <td>@conn.assigns.headline</td>
    <td><%= @conn.assigns.headline %></td> (1)
  </tr>
  <tr>
    <td>@headline</td>
    <td><%= @headline %></td> (2)
  </tr>
</table>
1 We can access the value of headline through the longer @conn.assigns.headline.
2 But normaly we access it via the shortform @headline. The @-version is accessable for all subcontent of @conn.assigns.
http://localhost:4000/playground

Static Clock

The current application always displays the same content. The easiest way to change that is to display the time. For that we add a timestamp variable in the controller and display it in the template:

lib/hello_world_web/controllers/page_controller.ex
[...]
def playground(conn, _params) do
  headline = "This is a test headline"
  {:ok, timestamp} = DateTime.now("Etc/UTC")

  conn
  |> assign(:headline, headline)
  |> assign(:timestamp, timestamp)
  |> render("playground.html")
end
[...]
lib/hello_world_web/templates/page/playground.html.eex
<h1><%= @headline %></h1>

<table>
  <tr>
    <td>Etc/UTC</td>
    <td><%= @timestamp %></td>
  </tr>
</table>
http://localhost:4000/playground

The web consists of webpages which link to each other. So the next step on our venture for the ultimate Phoenix application is a game of ping-pong. http://localhost:4000/ping will display a link to http://localhost:4000/pong and vice versa.

$ mix phx.new game --no-ecto
[...]
$ cd game
$ mix phx.server

First, we have to set the routes for ping and pong:

lib/game_web/router.ex
defmodule GameWeb.Router do
  [...]

  scope "/", GameWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/ping", PageController, :ping (1)
    get "/pong", PageController, :pong (2)
  end

  [...]
1 Sets the route for http://localhost:4000/ping
2 Sets the route for http://localhost:4000/pong

Next we add the actions to the PageController:

lib/game_web/controllers/page_controller.ex
defmodule GameWeb.PageController do
  use GameWeb, :controller

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

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

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

And the ping.html.eex template:

lib/hello_world_web/templates/page/ping.html.eex
<h1>Ping</h1>

Perfect. What a nice ping we have created page:

http://localhost:4000/ping

The missing pong counter part is easy:

lib/hello_world_web/templates/page/pong.html.eex
<h1>Pong</h1>

But for Ping-Pong, we need a href link between both pages. We could add one manually with <a href="/pong">Pong</a> but that would not be very clean. Since we have a router in Phoenix, we can use that to create clean routes for our links.

We either have to stop the Phoenix server (CTRL-C twice!) or open a new terminal with the same direction to run a mix phx.routes which returns all known routes. Because we are only interested in the routes for PageController we grep for those:

$ mix phx.routes | grep PageController
  page_path  GET  /       GameWeb.PageController :index
  page_path  GET  /ping   GameWeb.PageController :ping (1)
  page_path  GET  /pong   GameWeb.PageController :pong
1 For us important is the page_path and the :ping and :pong.

With that information, we can use the link helper to create that link:

lib/game_web/templates/page/ping.html.eex
<h1>Ping</h1>

<p>
<%= link "Pong!", to: Routes.page_path(@conn, :pong) %> (1)
</p>
1 page_path and :pong action become Routes.page_path(@conn, :pong)

We do the same on the pong page:

lib/game_web/templates/page/pong.html.eex
<h1>Pong</h1>

<p>
<%= link "Ping!", to: Routes.page_path(@conn, :ping) %> (1)
</p>
http://localhost:4000/ping

Now you can play HTML Ping-Pong.

You’ll find the syntax for links with specific params or queries in the Router chapter.

Sometimes your design team wants to add a specific CSS class to a link. Here’s how you do that:

lib/game_web/templates/page/pong.html.eex
<h1>Pong</h1>

<p>
<%= link "Ping!", to: Routes.page_path(@conn, :ping, class: "btn") %> (1)
</p>
1 Just add a class: "whatever" to the link helper.

Static files

Of course, any web application doesn’t only have dynamic webpages but also some static files. The best example would be a robots.txt or a favicon.ico file. There is the assets/static/ directory where we can put those files. By default the following files are already in that directory:

$ tree assets/static/
assets/static/
├── favicon.ico
├── images
│   └── phoenix.png
└── robots.txt

They get delivered by the production web server without any additional interaction with the Phoenix application. In development, there is some interaction, but that has a small footprint.

But adding a file to that directory is not enough. You have to whitelist it else, Phoenix won’t know what you want. Assuming we add an ads.txt file into the assets/static/ directory. Then we have to update the lib/hello_world_web/endpoint.ex file accordingly:

lib/hello_world_web/endpoint.ex
[...]

plug Plug.Static,
  at: "/",
  from: :hello_world,
  gzip: false,
  only: ~w(css fonts images js favicon.ico robots.txt ads.txt) (1)

[...]
1 All static files or directories have to be whitelisted in this list.

Images

Images are a particular case of static files. They can be stored in the assets/static/images/ directory which is by default whitelisted to include static files.

In every fresh Phoenix installation you’ll find the Phoenix logo file stored at assets/static/images/phoenix.png. That image is used in the default app.html.eex and there we can have a look how to access that image from within .eex:

$ grep "phoenix.png" lib/demo_web/templates/layout/app.html.eex
<img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>

You can use Routes.static_path(@conn, "/images/phoenix.png") to address the image in any .eex.

CSS

As written in the Preface: We’ll not waste time in this book by making our webpages pretty. But in case you want to add some CSS into your demo application, you can do so by referencing the assets/css/app.scss file.