Plugs Are to Elixir What Rack Is to Ruby

Datetime:2016-08-23 05:12:39          Topic:          Share

Plugs are to Elixir what Rack is to Ruby . As such, they both use functions to manipulate web requests. The principle behind plugs is simple, as is their implementation. In this article, we’ll explore basic plug usage, from “hello world” to a JSON API.

Prerequisites

  1. Elixir and a grasp of basic Elixir syntax and concepts .
  2. Basic knowledge of web requests and HTTP.

The Project

To begin, we are going to create a basic Elixir project using mix . Let’s call our project, Spy .

$ mix new spy

* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/spy.ex
* creating test
* creating test/test_helper.exs
* creating test/spy_test.exs

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

    cd spy
    mix test

Run "mix help" for more commands.

If this output looks unfamiliar, take a look at this guide on mix so you can be up to speed.

Now it’s time to dive in: First we must add Plug and a web server (Cowboy) to our project’s mix.exs . Then we need to add them to our application’s dependencies ( Yes , there is a difference, and yes , I know it’s confusing.) Here’s the finished product:

#mix.exs

defmodule Spy.Mixfile do
  use Mix.Project

  def project do
    [app: :spy,
     version: "0.1.0",
     elixir: "~> 1.3",
     buildembedded: Mix.env == :prod,
     startpermanent: Mix.env == :prod,
     deps: deps()]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [applications: [:logger, :cowboy, :plug]]
  end

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [
      {:cowboy, "~> 1.0.0"},
      {:plug, "~> 1.0"}
    ]
  end
end

Now we can install the dependencies we just specified with mix deps.get . Once this is complete, we can start building out our project. We’re going to begin by creating a plug that responds to any GET request with Hello, world! . To do this, we are going to build out our Spy module like so:

#lib/spy.ex

defmodule Spy do
  import Plug.Conn

  def init(options), do: options

  def call(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain") # another plug
    |> send_resp(200, "Hello, world!")
  end
end

After importing the Plug.Conn module, we defined two functions: init/1 and call/2 . These are the only two functions needed to make a module plug.

Looking deeper into the call/2 function, we can see that it also calls two plugs. This is the beauty of plugs: as they are just pure functions that manipulate a connection, they are, consequently, composable . This means that any plug is likely to call several other plugs to do its work. Remember: plugs are just functions that accept and return a connection. It’s that simple.

Let’s spin up a server to test out our plug.

$ iex -S mix
iex> {:ok, _pid} = Plug.Adapters.Cowboy.http(Spy, [])
# => {:ok, #PID<0.201.0>}

$ curl http://localhost:4000/
Hello, world!

Aside:While in most cases we would not want to start our server in such a long-winded fashion, these examples are intended to be more didactic than realistic, so this method will suffice. This server process will run as long as you have IEx open, so to restart after changing code, it is implied that you will restart the server by either running this line of code again or typing recompile , then pressing the up arrow and finding the line in your history.

So we just tested our plug with curl and it worked great! Let’s take this a step further and return some JSON.

Step 1:Add JSON parser to the application.

We are going to use Poison as our JSON encoder. Let’s add it to our application:

#mix.exs

defmodule Spy.Mixfile do
  use Mix.Project

  def project do
    [app: :spy,
     version: "0.1.0",
     elixir: "~> 1.3",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  def application do
    # Add Poison as application dependency.
    [applications: [:logger, :cowboy, :plug, :poison]]
  end

  defp deps do
    [
      {:cowboy, "~> 1.0.0"},
      {:plug, "~> 1.0"},
      # Add Poison as Hex dependency.
      {:poison, "~> 2.2"}
    ]
  end
end

Now that Poison is added, we need to change the response type to application/json and then encode a Map to JSON. It’s as simple as changing 3 lines of code:

#lib/spy.ex

  defmodule Spy do
  # ... (concatenated for brevity)
  def call(conn, _opts) do
    # encode to JSON
    body = %{body: "Hello, world!"} |> Poison.encode!

    conn
    |> put_resp_content_type("application/json") # JSON type
    |> send_resp(200, body) # send it!

  end
  # ...
end

Let’s test this again with curl . NOTE: If you get a message about missing dependencies and running mix deps.get , then you should, um, run mix deps.get . ANOTHER NOTE: You can get our of iex with a CTRL+C.

$ curl http://localhost:4000/
{"body": "Hello, world!"}

It worked! Now that we have the fundamentals of plugs down, let’s create Spy’s main functionality: listing cookies. Not the most glamorous function, but a useful one nonetheless. Let’s make a plan on how to implement this:

  1. Get cookies from response
  2. Encode cookies to JSON
  3. Set a new cookie for demonstration purposes

Now that we have a plan, it should be simple to execute step-by-step. Let’s dive in to the first step: Get cookies from response . It sounds simple, but how exactly should we implement it? Luckily Elixir packages are (mostly) beautifully documented, so we can easily find the right function . With a quick search of “cookie”, a list of options pops up. Which one? It looks like the fetch_cookies/2 function/plug is exactly what we’re looking for. So let’s add this to our app by redefining what gets passed to our main plug, and knock out step 2 in the process:

#line 7 of lib/spy.ex

body = conn |> fetch_cookies |> Map.get(:cookies) |> Poison.encode!

This line simply pipes the connection into the fetch_cookies plug, which then loads the cookies. These cookies, along with the connection, are fetched through the Map.get/2 function, and then encoded to JSON. That’s it! We just completed 2/3 of the steps, but we still have one more: Set a new cookie for demonstration purposes .

For our example, we’re going to make a simple cookie that assigns the current date (in string form) to a cookie with the key hello . If we look back at our Plug docs and search, we can see that put_resp_cookie/4 is perfect for this. To add our cookie, we simply put this plug right after the initial call of conn in our call/2 function. Let’s take a look at the finished product:

#lib/spy.ex
defmodule Spy do
  import Plug.Conn

  def init(options), do: options

  def call(conn, _opts) do
    body = conn |> fetch_cookies |> Map.get(:cookies) |> Poison.encode!

    conn
    |> put_resp_cookie("hello", DateTime.utc_now |> DateTime.to_string)
    |> put_resp_content_type("application/json") # another plug
    |> send_resp(200, body)
  end
end

Now we should restart the server and open up http://localhost:4000/ , but this time in the browser. On first load you may see a blank page. This is good! It means you have no cookies stored on your browser. Now if you reload the page, you will see that our plug assigned a new cookie with the current time (the time when the cookie was assigned).

Why don’t I see my cookie on first reload? Because of the way that our plug is setup, we only see the cookie after a reload because body is assigned before the cookie is set.

In Conclusion

What we just created only shows a minuscule amount of plug’s full potential. Plugs make up the basis of Phoenix in the same way Rack makes up the basis of Rails. With plugs you can make anything, from simple HTTP services to robust APIs. Combine that with the rapidly growing Elixir community and the pure speed and failsafe nature of Erlang, and you’re on your way to developer paradise.