Building web apps with Plug.Router

25 Jan 2019 · by Sean Callan in General

When it comes to building a web application with Elixir many people will immediately reach for Phoenix. However, did you know Plug.Router is just as viable an option? Sometimes, it can be even faster.

The project

For this project we’ll build a simple single page portfolio site. We can expect our site to load and display our portfolio from a file, database, or somewhere else dynamically. As well as allowing users to submit contact information via a web form.

Please note: To keep the application and tutorial concise, we will forego database backing and stub this out in favor of concentrating on the web portions.

Want to skip the reading and just look at a code? Head over to elixirschool/router_example.

Getting started

To get started we need to do a couple of things:

  1. Generate a new project with mix new --sup
  2. Add the plug_cowboy dependency to mix.exs
  3. Put our router into the supervision tree of our application

Without further delay, let’s get this show on the road and generate our new project:

$ mix new router_example --sup

* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/router_example.ex
* creating lib/router_example/application.ex
* creating test
* creating test/test_helper.exs
* creating test/router_example_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd router_example
    mix test

Run "mix help" for more commands.

Let’s head into our new directory and open mix.exs in the editor of your choice. Here we’ll make small a small change by adding the plug_cowboy dependency:

defp deps do
  [
    {:plug_cowboy, "~> 2.0"}
  ]
end

With that change in place we can fetch our dependencies with mix deps.get and proceed. Though we have not yet created our router, let’s get the supervisor setup out of the way. We’ll need to open lib/router_example/application.ex next so we can update our supervisor’s children. The plug_cowboy package makes this step easy with the included Plug.Cowboy.child_spec/3 function. Let’s update our application’s start/2 function:

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

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

Our setup is complete! Before we can run our application we’ll need to create our RouterExample.Router module which we’ll do next.

Stubs

As was mentioned in the introduction for the sake of keeping the tutorial short and focused, we’ll be stubbing the functions that would otherwise retrieve data from our datastore and persist it there. We won’t waste too much time on this file we just need two functions: one to give us a collection of results (stubbing a database lookup) and a second to take our parameters and write them to the terminal (instead of persisting them to a store).

Let’s create a new file lib/router_example/stubs.ex and copy the following code into it:

defmodule RouterExample.Stubs do
  def portfolio_entries do
    for x <- 1..10, do: %{name: "Project #{x}", image: "https://picsum.photos/400/300/?random?t=#{x}"}
  end

  def submit_contact(params) do
    IO.inspect(params, label: "Submitted contact")
  end
end

Note: If you’re unfamiliar with the :label option in IO.inspect/2, check out our other blog post TIL IO.inspect labels.

The Router

One small line of code, use Plug.Router, unlocks great potential by bringing the power of Plug.Router into our application. Need a refresher on use/1? Head on over to Elixir School’s section on use.

So what is Plug.Router afterall?

Stated simply Plug.Router is a collection of macros that make it easy to match request paths and their type. If you’re familiar with with Ruby’s Sinatra, Python’s Flask, Java’s Jersey, or any of the other “micro frameworks” then this will look familiar.

Create a new file for our router and open it: lib/router_example/router.ex. Let’s copy the basic router code below into our new file and then look at the individual pieces:

defmodule RouterExample.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/ping" do
    send_resp(conn, 200, "pong")
  end
end

The first thing we notice after the use is a series of plugs: match and dispatch. These are included for us and match the incoming request to a function and invoke it respectively.

The next thing we see is our first route! It’s a simple health check but it’s significant none-the-less, it shows us the format all of our routes will follow: HTTP verb, path, and block of code. As one migth imagine there is more than get/1, we also have post/1, patch/1, put/1, delete/1, option/1, and match/1.

In the next few sections we’ll explore some of these other macros but first let’s look at how we can handle rendering EEx templates and JSON.

For more on Plug.Router check out the dedicated section in our Plug lesson.

Rendering templates

Elixir includes EEx, Embedded Elixir, which lets us parse strings and files and evaluate the embedded Elixir. For our purposes we’ll focus on EEx.eval_file/2 and use it as the basis for our own render/3 function.

For our render/3 function we’ll pass in our connection struct, a template without the “.eex” extension, and any variable bindings we may want to provide to the embedded Elixir. We want a function invocation that will look something like this:

render(conn, "index.html", portfolio: [])

Now that we know what we want it’s time to build it!

The EEx.eval_file/2 function takes our template’s filepath along with variable bindings and returns the computed string, our response body. With EEx doing all the heavy lifting our render/3 function needs to do little more than build the complete filepath and send along the response with send_resp/3:

@template_dir "lib/router_example/templates"

...

defp render(%{status: status} = conn, template, assigns \\ []) do
  body =
    @template_dir
    |> Path.join(template)
    |> String.replace_suffix(".html", ".html.eex")
    |> EEx.eval_file(assigns)

  send_resp(conn, (status || 200), body)
end

We’ve set it up for EEx to look for our templates in the directory lib/router_example/templates so let’s create that directory. Next we’ll create two templates index.html.eex and contact.html.eex.

You can find the code for index.html.eex here and contact.html.eex here, we won’t focus on HTML or CSS today.

Sending and recieving JSON

Building JSON endpoints in Plug.Router is much less work than the template rendering we just covered.

The first thing we need is a library to parse and encode JSON, for our purpose we’ll use jason:

{:jason, "~> 1.1"}

Now would be a good time to run mix deps.get before we forget. Once we’ve done that we can move on to updating our router to handle incoming JSON via the Plug.Parsers plug. Let’s open lib/router_example/router.ex and update our plugs to include Plug.Parsers with Jason as our decoder:

plug Plug.Parsers, parsers: [:json],
                   pass: ["text/*"],
                   json_decoder: Jason
plug :match
plug :dispatch

That’s really all we need to do for JSON.

If we want to keep things simple, we can leverage Jason.encode/1 or Jason.encode!/1 along with send_resp/3 and be done:

{:ok, json} = Jason.encode(result)
send_resp(conn, 200, json)

Or if we want a little more polish we could make a render_json/2:

defp render_json(%{status: status} = conn, data) do
  body = Jason.encode!(data)
  send_resp(conn, (status || 200), body)
end

For the remainder of the post we’ll use the render_json/2 approach.

Note: If you intend to use something like the JSON:API specification, you may want an additional dependency like jsonapi to help.

Defining routes

We now have our router code in place, code to render templates, and code to render JSON, all that’s left is for us to define our routes!

We previously created two EEx templates, index.html.eex and contact.html.eex, so we’ll create routes to handle those requests first. From our healthcheck endpoint we know the format we expect but we can use our new render/3 functions as well as our stubbed data.

If you looked a index.html.eex then you know our EEx expects a portfolio capture populated with a list of maps containing :name and :image, the format we conveniently defined in our stubbed module! Let’s bring all the pieces together inside lib/router_example/router.ex:

get "/" do
  render(conn, "index.html", portfolio: Stubs.portfolio_entries())
end

get "/contact" do
  render(conn, "contact.html")
end

We’re getting there but we’re not quite done. We have to handle our contact form’s AJAX requests.

In the interest of staying focused we won’t get sidetrack with validation today, instead we’ll make use of our Stubs.submit_contact/1 function. Let’s create a new route with the post/1 macro that uses the aforementioned function and sends a pleasant JSON message back using our render_json/2 function:

post "/contact" do
  Stubs.submit_contact(conn.params)
  render_json(conn, %{message: "Thank you! We will get back to you shortly."})
end

That’s it, we’re done, right?! Well — we probably want to handle requests to routes we haven’t defined yet. Let’s do that next and then we can call it done.

Missing routes

Handling missing routes is a straight forward thanks to Elixir’s powerful pattern matching. With the match/3 macro and _ we can match on all requests. By placing this at the bottom of our router we ensure if a request has not previously been matched it will be caught and handled.

For now we’ll implement a simple message, similar to how we implemented our “/ping” endpoint with send_resp/3:

defmodule RouterExample.Router do
  use Plug.Router

  ...

  match _ do
    send_resp(conn, 404, "Oh no! What you seek cannot be found.")
  end
end

Tada! Our app is complete, time to wrap things up.

Wrapping things up

We’ve come a long way in very small lines of code, let’s look the entirety of our app:

defmodule RouterExample.Router do
  use Plug.Router

  alias RouterExample.Stubs

  @template_dir "lib/router_example/templates"

  plug Plug.Parsers, parsers: [:urlencoded, :json],
                   pass: ["text/*"],
                   json_decoder: Jason
  plug :match
  plug :dispatch

  get "/ping" do
    send_resp(conn, 200, "pong")
  end

  get "/" do
    render(conn, "index.html", portfolio: Stubs.portfolio_entries())
  end

  get "/contact" do
    render(conn, "contact.html")
  end

  post "/contact" do
    Stubs.submit_contact(conn.params)
    render_json(conn, %{message: "Thank you! We will get back to you shortly."})
  end

  match _ do
    send_resp(conn, 404, "Oh no! What you seek cannot be found.")
  end

  defp render(%{status: status} = conn, template, assigns \\ []) do
    body =
      @template_dir
      |> Path.join(template)
      |> String.replace_suffix(".html", ".html.eex")
      |> EEx.eval_file(assigns)

    send_resp(conn, (status || 200), body)
  end

  defp render_json(%{status: status} = conn, data) do
    body = Jason.encode!(data)
    send_resp(conn, (status || 200), body)
  end
end

We’re rendering EEx templates, receiving and sending JSON, and all the entire app (RouterExample.Router + RouterExample.Stubs) is 58 lines of code!

All that’s really left for us to do is run it and enjoy our new website. To run our new app we’ll use mix run --no-halt, the app can be found at localhost:4001.

There is no arguing this is a very basic implementation but it get’s us started. With these simple pieces we have what we need build to something significant.

We hope you’ve enjoyed! In future posts we’ll explore improvements like grouping routes together into module, supporting Webpack, other refactors.

The code for our application can be found at elixirschool/router_example.

Article tags

plug software design

Sean Callan

Sean has been passionately involved with Elixir since the very beginning. After experiencing the joys of working with Elixir he created Elixir School and has become a core contributor to numerous libraries. During the day, Sean helps companies transition teams and codebases to Elixir.