Adding Phoenix to the mix

Published November 03, 2018 by Toran Billups

In part 6 of the url shortener adventure we bolt on a web front-end with help from Phoenix.

Part 6: Phoenix

In the previous post we made our application more resilient by using the filesystem to store process state. Next we need to make our application accessible from the web and this finally gave me an excuse to play around with the Phoenix web framework!

The first step of any Phoenix tutorial is using the mix command to generate a scaffold. Because I like to understand all the files that are generated and only commit what is absolutely necessary you will find that I've cut out a handful of things to keep it beginner friendly.

    mix phx.new example --no-ecto --no-brunch
  

note: when Phoenix 1.4 lands the `--no-brunch` you see above will be replaced with `--no-webpack`

And because I'm using the scaffold generated as a guide, I'll be showing a few steps that usually don't get much love in a more traditional walk through. The first of which is that I need to add a few dependencies to the mix.exs file so Phoenix and friends are available.

    defp deps do
      [
        {:phoenix, "~> 1.3.3"},
        {:phoenix_pubsub, "~> 1.0"},
        {:phoenix_html, "~> 2.10"},
        {:phoenix_live_reload, "~> 1.0", only: :dev},
        {:cowboy, "~> 1.0"},
        {:plug_cowboy, "~> 1.0"}
      ]
    end
  

The next step requires we alter the application.ex file to include the new endpoint module generated by mix. Note: the web application is using the module prefix `ExampleWeb` while the OTP application itself still has the prefix 'EX'. In a future refactor I'll update this `EX` prefix to be `Example` instead to better match the folder structure.

    defmodule EX.Application do
      use Application
    
      def start(_type, _args) do
        import Supervisor.Spec
    
        children = [
          supervisor(ExampleWeb.Endpoint, []),
          {Registry, keys: :unique, name: EX.Registry},
          EX.Cache,
          EX.Worker
        ]
    
        opts = [strategy: :one_for_one, name: EX.Supervisor]
        Supervisor.start_link(children, opts)
      end
    
      def config_change(changed, _new, removed) do
        ExampleWeb.Endpoint.config_change(changed, removed)
        :ok
      end
    end
  

At a glance the endpoint.ex file reveals a Plug pipeline that Phoenix will use to handle incoming requests. The important bit for today is the line `plug ExampleWeb.Router` because this will direct any http request to our application's Router.

    defmodule ExampleWeb.Endpoint do
      use Phoenix.Endpoint, otp_app: :example
    
      plug Plug.Logger
      plug Plug.Parsers,
        parsers: [:urlencoded, :multipart, :json],
        pass: ["*/*"],
        json_decoder: Poison
    
      plug Plug.MethodOverride
      plug Plug.Head
    
      plug ExampleWeb.Router
    
      def init(_key, config) do
        if config[:load_from_system_env] do
          port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
          {:ok, Keyword.put(config, :http, [:inet6, port: port])}
        else
          {:ok, config}
        end
      end
    end
  

The router is where you declare different contexts with `pipeline` (ie: api, browser) and the routes themselves. Today we will add the most basic `json` api allowing the application to store and retrieve short urls via http GET/POST. Once the router has determined a match it will forward on the request to our `UrlController`.

    defmodule ExampleWeb.Router do
      use ExampleWeb, :router
    
      pipeline :api do
        plug :accepts, ["json"]
      end
    
      scope "/api", ExampleWeb do
        pipe_through(:api)
    
        get("/:id", UrlController, :show)
        post("/create", UrlController, :create)
      end
    end
  

The controller implements 2 public functions that are called from the router. When a GET request is handled we call `show` passing the id for the given link. When a POST request is handled we call `create` and use the data payload to generate a short hmac like value that will act as the unique idenifier for the shortened url. Both functions use a helper called `render` that point to our view module.

    defmodule ExampleWeb.UrlController do
      use ExampleWeb, :controller
    
      def show(conn, %{"id" => id}) do
        url = EX.Worker.get(:worker, "#{id}")
        render(conn, "show.json", %{id: id, url: url})
      end
    
      def create(conn, %{"link" => %{"url" => url}}) do
        id = hmac(url)
        EX.Worker.put(:worker, id, url)
        render(conn, "show.json", %{id: id, url: url})
      end
    
      defp hmac(url) do
        :crypto.hmac(:sha256, "example", url)
          |> Base.encode16
          |> String.slice(0, 6)
      end
    end
  

The view has a `render` function that will pattern match on the inputs provided by the controller and return a map. This step isn't necessary at the moment because of how simple the use case is but I went ahead with it to see the flow from Router => Controller => View.

    defmodule ExampleWeb.UrlView do
      use ExampleWeb, :view
    
      def render("show.json", %{id: id, url: url}) do
        %{id: id, url: url}
      end
    end
  

To see this in action we next write an integration test to exercise the router, controller and view. You can run this from the command line using `mix test`. To list out all of the routes available use the command `mix phx.routes`

    defmodule ExampleWeb.UrlControllerTest do
      use ExampleWeb.ConnCase
    
      test "GET and POST drive the url shortener", %{conn: conn} do
        id = "77B5F6"
        url = "google.com"
        default_result = get(conn, url_path(conn, :show, id))
        default = %{"id" => id, "url" => "undefined"}
        assert json_response(default_result, 200) == default
    
        expected = %{"id" => id, "url" => url}
        post_result = post(conn, url_path(conn, :create, %{link: %{url: url}}))
        assert json_response(post_result, 200) == expected
    
        get_result = get(conn, url_path(conn, :show, id))
        assert json_response(get_result, 200) == expected
      end
    end
  

You can also verify the application works end to end with curl. First spin up the Phoenix app from the command line.

    iex -S mix phx.server
  

Next make a POST request with curl to insert the url you'd like to shorten with the api.

    curl --request POST \
    --url http://localhost:4000/api/create \
    --header 'content-type: application/json' \
    --data '{"link": {"url": "google.com"}}'
  

Finally make a GET request with curl to query the api for a url by id.

    curl --request GET \
    --url http://localhost:4000/api/77B5F6 \
    --header 'content-type: application/json'
  

You can track my progress on github commit by commit. If you just want the code for this post checkout this commit.


Buy Me a Coffee

Twitter / Github / Email