Plugs Are to Elixir What Rack Is to Ruby

Jesse Herrick
Jesse Herrick
Share
elixir

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.

Frequently Asked Questions (FAQs) about Elixir Plugs

What is the main purpose of Elixir Plugs?

Elixir Plugs are a specification for composable modules in web applications. They provide a unified interface for processing web requests. This means that you can create a series of plugs, each doing a specific task, and chain them together to handle a web request. This makes your code modular, reusable, and easy to understand.

How does Elixir Plugs compare to Rack in Ruby?

Both Elixir Plugs and Rack in Ruby serve similar purposes – they provide a common interface for web servers and applications. However, Elixir Plugs offer more flexibility due to their composable nature. You can create a series of plugs, each doing a specific task, and chain them together to handle a web request. This is different from Rack where the middleware stack is more rigid.

How do I install and use Elixir Plugs?

To install Elixir Plugs, you need to add the plug dependency to your mix.exs file and run the mix deps.get command. Once installed, you can create a plug by defining a module and implementing two callback functions: init/1 and call/2. The init function is used to initialize your plug’s options, while the call function is used to process the request.

What are the different types of Plugs?

There are two types of plugs: function plugs and module plugs. Function plugs are simple functions that take a connection and a set of options and return a connection. Module plugs are modules that implement the Plug behaviour, meaning they must define two functions: init/1 and call/2.

How do I chain multiple plugs together?

You can chain multiple plugs together using the plug keyword in your router. The plugs are executed in the order they are defined. Each plug takes the connection as the first argument and a set of options as the second argument. The connection is passed from one plug to the next, allowing each plug to modify the connection as needed.

How do I handle errors in Elixir Plugs?

You can handle errors in Elixir Plugs by using a try/rescue block in your call function. If an error occurs, you can modify the connection to send a response with an appropriate status code and message.

Can I use Elixir Plugs for authentication?

Yes, you can use Elixir Plugs for authentication. You can create a plug that checks if a user is authenticated before processing a request. If the user is not authenticated, you can modify the connection to redirect the user to a login page.

How do I test Elixir Plugs?

You can test Elixir Plugs by creating a connection with the Plug.Test module and passing it to your plug. You can then make assertions about the returned connection, such as the status code, headers, and body.

Can I use Elixir Plugs with Phoenix?

Yes, you can use Elixir Plugs with Phoenix. In fact, Phoenix is built on top of Plug. You can use plugs in your Phoenix controllers, or you can use them in your router to process requests before they reach your controllers.

What are some best practices for using Elixir Plugs?

Some best practices for using Elixir Plugs include keeping your plugs small and focused, testing your plugs thoroughly, and using plugs for reusable functionality such as authentication and authorization.