Plugs Demystified

Understand Plug and build a simple website using Elixir

Is Phoenix the only way?

When I first heard about Elixir what really piqued my interest about it was Phoenix's speed. Coming from a Rails background, sub-millisecond response times some of my colleagues talked about felt almost surreal. I eventually wanted to see what it was all about myself and that is how I started reading Elixir docs.

I presume I am not the only one who reached for Elixir because of Phoenix. It is the default web framework most of us reach for in the Elixir world. This fact, however, leads to a somewhat academic question - is it possible to build a simple web service without using Phoenix and, if so, how hard would it be?

Meet Plug

One of the most obvious alternatives is Plug. Plug is a library providing a uniform interface for talking to web servers in the ErlangVM and an easy way to build composable modules (aka plugs) which can be put together to process web requests.

The two key parts of the Plug ecosystem are the Conn struct and plugs which manipulate it. The Conn struct is what represents a full HTTP request/response cycle. It includes request and response bodies, query params, cookies, IP data and a myriad of other things which you can inspect and manipulate. Plugs, on the other hand, are modules or functions which take a Conn and return a modified version of it. A plug can modify a Conn in a number of ways such us redirect to an HTTPS version of a URL, verify security headers, log the incoming request, or set a response code and a body.

It is an easy and efficient system - build some plugs and chain them together to process an incoming Conn.

Conns and plugs

Plug is, in fact, the foundation on which Phoenix is built - Phoenix's controllers are just plugs! Let's put Phoenix aside for now though and see how easy it is to use Plug directly.

The aim

We are going to build a simple website displaying the current price of Bitcoin and calculating the maximum possible loss one could make assuming they invested in Bitcoin on the day it reached its peak price (at the time of writing this article the highest Bitcoin price ever was $19,498.63 on 17/12/2017). Instead of instinctively reaching for Phoenix, we'll only use Plug and see how easy the process is. The sketch below is a simple illustration of what we aim to achieve.

Page sketch

It is a fairly simple project which will serve us well as an example.

Code

Let's start by creating a new mix project. We'll use the --sup flag to include a supervision tree.

mix new loss_tracker --sup
cd loss_tracker

We need to add some dependencies to the mix.exs file. Other than including plug we will also add cowboy (the web server), httpoison (making HTTP requests to be Bitcoin price server) and poison (parsing JSON). Don't forget to run mix deps.get to install the new dependencies.

# mix.exs

# ...

defp deps do
  [
    {:cowboy, "~> 1.0.0"},
    {:plug, "~> 1.0"},
    {:httpoison, "~> 1.0"},
    {:poison, "~> 3.1"},
  ]
end

I am using Exlixir 1.6.1 here. If you are using 1.3 or lower remember to update your applications function too.

Now open up lib/loss_tracker/application.ex and tell our application to start Plug and integrate with Cowboy.

# lib/loss_tracker/application.ex

defmodule LossTracker.Application do
  @moduledoc false

  alias LossTracker.Router

  use Application

  def start(_type, _args) do
    children = [
      Plug.Adapters.Cowboy.child_spec(scheme: :http, plug: Router, options: [port: 4001])
    ]

    opts = [strategy: :one_for_one, name: LossTracker.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Here we tell Plug to integrate with Cowboy and pass all incoming traffic to LossTracker.Router. All requests will be delivered to the router as Conn structs we talked about earlier. We have not written our router yet so we'll look at it next.

# lib/loss_tracker/router.ex

defmodule LossTracker.Router do
  use Plug.Router

  alias LossTracker.PageProcessor

  plug :match
  plug :dispatch

  get "/about" do
    send_resp(conn, 200, "This is a demo site")
  end

  match _ do
    PageProcessor.call(conn, PageProcessor.init([]))
  end
end

The router is the first plug that our conn struct will encounter. This is also where we get to see the first example of how easily plugs can be used and chained together. We tell the router to pipe our conn through two plugs: match and dispatch. These two plugs are provided by Plug and take care of matching the request's path and dispatching it to the correct route handler. If you wanted to modify this behaviour you could write your own plugs and include them in there too.

Within the router, we specify two route handlers. The first one is an example of how the Plug router works - it matches GET requests sent to /about. Whenever a user navigates to that URL the conn struct will be passed to send_resp which, in turn, will set the response code to 200 and put a short string in the response's body using a special send_resp function. It is special because instead of simply returning a slightly modified conn, like most plugs do, it immediately sends the response back to the caller.

The second route handler matches all other URLs and will pass the conn to the PageProcessor module plug which we are going to write next.

# lib/loss_tracker/page_processor.ex

defmodule LossTracker.PageProcessor do
  import Plug.Conn

  alias LossTracker.BitcoinAPI

  def init(options), do: options

  def call(conn, _opts) do
    conn
    |> calculate_loss()
    |> build_page()
  end

  defp calculate_loss(conn) do
    current_price = BitcoinAPI.current_price()
    current_loss = BitcoinAPI.current_loss(current_price)

    conn
    |> assign(:current_price, current_price)
    |> assign(:current_loss, current_loss)
  end

  defp build_page(conn) do
    conn
    |> put_resp_content_type("text/html")
    |> send_resp(200, render_page(conn))
  end

  defp render_page(conn) do
    """
    <!DOCTYPE html>
    <html>
      <head>
        <title>Bitcoin loss tracker</title>
        <style>
          html {
            background-color: #252839;
            color: #f2b632;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
            font-size: 24px;
            text-align: center;
          }
          div {
            position: fixed;
            top: 50%; 
            left: 50%; 
            transform: translate(-50%, -50%);; 
            padding: 3rem;
            border: 6px solid #f2b632;
            border-radius: 6px;
          }
          p {
            margin: 1rem 0;
          }
          p:first-of-type {
            font-size: 0.8rem;
            line-height: 1rem;
          }
          p:last-of-type {
            font-size: 1.4rem;
          }
          .percentage {
            font-weight: 700;  
            font-size: 1.8rem;
          }
        </style>
      </head>

      <body>
        <div>
          <p>On December 17th 2017 Bitcoin price reached $#{BitcoinAPI.max_price()}</p>
          <p>Current Bitcoin value is $#{conn.assigns[:current_price]}</p>
          <p>You could have lost <span class="percentage">#{:erlang.float_to_binary(conn.assigns[:current_loss], [decimals: 1])}%</span> of your money!</p>
        </div>
      </body>

    </html>
    """
  end
end

The PageProcessor here is where the response is put together. Its call function accepts a conn and modifies it in a number of ways. First, it passes the conn to calculate_loss where it obtains the current price and current maximum possible loss from the BitcoinAPI module and puts them in the conn to be used later. Conn structs, apart from carrying information about requests and responses, can also carry other arbitrary data. This is exactly what we tell our conn to do here by calling assign.

The conn is then passed to build_page where it is assigned a content type of text/html and used to send a response back to the caller. The response is given a 200 status code and a body.

The body of the response is HTML returned from the render_page function. It prints the current price of Bitcoin and the maximum possible loss on the page and applies some styling to it.

Finally, the BitcoinAPI module. This is the only part of our app that doesn't use plugs. It is a simple interface for fetching the current Bitcoin price from one of the Bitcoin trading platforms which provides a free API.

# lib/loss_tracker/bitcoin_api.ex

defmodule LossTracker.BitcoinAPI do
  @max_price 19498.63
  @api_url "https://www.bitstamp.net/api/ticker/"

  def current_loss(current_price) do
    (@max_price - current_price) / @max_price * 100
  end

  def current_price() do
    case HTTPoison.get(@api_url) do
      {:ok, response} ->
        response.body
        |> Poison.decode!
        |> Map.get("last")
        |> String.to_float
      {:error, _} ->
        nil
    end
  end

  def max_price() do
    @max_price
  end
end

This is it! Start the server with mix run --no-halt (or iex -S mix if you want to play around in the IEx console) and navigate to localhost:4001. You should see something similar to the image below.

Finished page

Summary

In this short demo, we have had a look at what Plug library is, what tools it provides and how they can be used to put together a basic web application. We have seen that Plug gives us everything needed to build a simple website, even without using Phoenix. Of course, the ecosystem is not perfect and is devoid of numerous Phoenix's features. This is because Plug is not meant to be a web-framework. Instead, it is a web server interface we can use to build other things on and it is exactly what Phoenix does.

As an engineer, I believe it is important to keep improving our understanding of technologies underpinning the tools we work with. Even if you are not going to build directly on Plug, knowing how it works will help you leverage its power when working with Phoenix. After all, Phoenix is nothing else but a giant plug.