Learning Elixir Plugs by Finding Books

Alex Koppel bio photo By Alex Koppel

In the Renaissance and early modern Europe, book readers would would frequently collect interesting quotes, observations, and other ideas into volumes they called commonplace books. Commonplacing was a way to track and organize ideas, significant to the point that Oxford formally taught its students how to keep them. As someone ran across cool or striking passages in, say, Milton’s Paradise Lost, they’d jot it down for the future.

Nowadays, we have Twitter.

This summer, I’ve been building Commonplace Feeds1, a small personal app to collect and share my thoughts using the Elm+Elixir Starter Kit. Finishing my first fully-tested CRUD controller was a milestone in my Elixir career — I could finally create, read, update, and delete books. The second controller should have gone much faster, being a copy of the first, but there was a roadbump: I was duplicating code to load and validate books and quotes.

The Challenge

When updating or deleting books, we want to make sure both that the book record exists and the user has access to it. When working with quotes or thoughts, we need to apply those book filters and also similar ones for quotes (a quote should exist and belong to a book we’ve verified the user has access to).

In the original Rails version of the app2, this was easy to do using the before_actions that run before each web request:

before_action :ensure_book
before_action :ensure_quote, only: [:update, :destroy]

def book
  @book ||= Book.find_by(id: params.require(:book_id))
end

def ensure_book
  render json: {error: :not_found}, status: 404 unless book
end

def quote
  @quote ||= Quote.find_by(id: params.require(:id))
end

def ensure_quote
  render json: {error: :not_found}, status: 404 unless quote
end

Phoenix doesn’t have before_actions, though, and Elixir doesn’t have state (no memoization via ||=). So, what do we do?

Plugs

Meet plugs, the Elixir way of solving this problem.

Plugs are, in brief, a middleware system for handling web requests in Elixir. Each plug in a pipeline gets an incoming request (a connection object) in sequence. It can take any action on the request, ultimately either passing the request on to the next plug or ending the pipeline.

Plugs can be set up at both the application layer and individual controllers — conceptually, they’re like Rack middlewares and Rails before_actions rolled into one (and no doubt similar features in other frameworks).

There are two kinds of plugs, ones defined as regular functions and ones defined as modules3. I decided to write a module plug so I could set up resource loading for all my controllers — getting records is a pretty common need, after all.

Loading a Resource via a simple Plug

Let’s take a look at the code, first all at once and then section by section in detail. Don’t worry if it doesn’t all make sense now; we’ll dig into it soon.

defmodule CommonplaceFeeds.LoadBook do
  # Import methods to work with requests (the conn object), such as accessing
  # cookies or stored values, triggering responses, etc.
  import Plug.Conn

  # Import methods to interact with the database
  import Ecto.Query

  # Allow us to access modules without the CommonplaceFeeds prefix
  alias CommonplaceFeeds.{Repo, Book}

  # Initialize the plugin on load
  def init(default_opts), do: default_opts

  # Handle each request that comes in (the conn object)
  def call(conn, _opts) do
    id = conn.params["id"]

    book = Repo.one(from row in Book, where: row.id == ^(id))

    if book do
      assign(conn, :book, book)
    else
      conn
      |> put_resp_content_type("application/json")
      |> send_resp(404, Poison.encode!(%{"error" => "book with id #{id} not found"}))
      |> halt
    end
  end
end

That’s some code! What does it all do?

In your controller

Before we dig into the plug itself, let’s look at how we use it in the controller:

defmodule CommonplaceFeeds.BookController do
  # define this as a controller
  use CommonplaceFeeds.Web, :controller

  # add the plug and specify when to invoke it
  plug CommonplaceBooks.LoadBook when action in [:update, :delete]

  # a simple delete method as an example.
  def delete(conn, _params) do
    Repo.delete(conn.assigns[:book])
    json %{"ok" => true}
  end
end

This is, I think, pretty straightforward (at least if you’ve used MVC frameworks before). When a request comes in for which the plug should be invoked, it’s run before the method; any data it stores is available when the requested action is run.

The Plug’s init method

def init(default_opts), do: default_opts

init lets you setup up data you’ll need for your plug. Unlike call, init is executed at compile time4; it receives a list of any options specified in your controller, pipeline, etc., and returns a value that’s provided each time call is called.

(In the next blog post, I’ll go into more detail on call vs. init as well as ways to extend the LoadBook plugin — stay tuned.)

The call method

def call(conn, opts) do
  # ...
end

call is the meat of the module. As a request works its way through a pipeline, the pipeline will call the call method of each plug in turn. It takes conn (the request) as the first argument and then any options defined by init, the controller, etc.

Like any Elixir method, you can pattern match on call (or init). For example:

def call(conn, check_auth: true) do
  # some logic
end

def call(conn, check_auth: false) do
  # more different logic
end

call always returns a Plug.Conn object, a version of conn with any changes (if any) you’ve made. Plug.Conn defines a set of useful methods, each of which returns a copy of conn, including:

  • assigns(conn, key, value): add a value that subsequent plugs or controllers can access
  • put_resp_content_type(conn, content_type): set the content type of the response
  • halt(conn): end processing the plug pipeline

So, our plug looks for a Book database record matching the id given in the query. If it finds one, it adds it to the request for the controller or later plugs to use; if not, there’s no point in going forward5, so it halts processing and returns a 404 Not Found.

def call(conn, _opts) do
  id = conn.params["id"]

  book = Repo.one(from row in Book, where: row.id == ^(id))

  if book do
    assign(conn, :book, book)
  else
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(404, Poison.encode!(%{"error" => "book with id #{id} not found"}))
    |> halt
  end
end

Testing it out

Hopefully at this point we all understand how this basic plug works. Let’s try it out by writing a test for the controller method we defined above:

defmodule CommonplaceFeeds.BookControllerTest do
  use CommonplaceFeeds.ConnCase

  describe "#delete" do
    # create a book record so that we can delete it later
    setup %{user: user} do
      book = insert(:book, user_id: user.id)
      {:ok, [book: book]}
    end

    test "requires a valid book ID", %{conn: conn} do
      conn = delete conn, book_path(conn, :delete, 12345)
      assert json_response(conn, 404) == %{"error" => "book with id 12345 not found"}
    end

    test "deletes a book", %{book: book, conn: conn} do
      conn = delete conn, book_path(conn, :delete, book)
      deleted_book = Repo.get(Book, book.id)
      refute deleted_book
    end
  end
end

And it works! It properly loads the book when a legit id is given and it rejects the request when the id doesn’t match any records.

What’s next?

If I wrapped things up here and you tried use this code, you’d quickly run into some limitations:

  • You could access any book because it has no access control
  • We’d have to duplicate code for other models because it’s Book-specific
  • You can’t use more than one LoadModel plug in the same controller because it stores the data in a fixed place
  • You can’t reuse it for the same resource if the ID parameter key changes (for instance, nested resources where the book is specified under book_id)

Fortunately, I’m not going to wrap things up here (at least not for long). In my next blog post, I’ll iterate over LoadBook to solve each of these problems (and more) in turn, leaving us with a lightweight, useful plug for loading and validating resources.

Researching and writing this was hugely useful to me, and I hope it’s been interesting and informative for you as well! If you have any questions, found this useful, spotted a mistake, I’d love to hear from you.

Have a great week!

  1. the name Commonplace Books being already snagged by the people behind Welcome to Night Vale. It’s not clear if they’re still using it, but they got there first. 

  2. a first version as a Rails+Elm project has been collecting quotes for about four months. Because I never finished it, porting the project to Elixir+Elm is quite achievable. 😊 

  3. For a great introduction to the plug system, see this blog post by Brian Storti

  4. If you’re a Rubyist, think about how methods like before_action, along with their arguments, are excuted when the file loads, not when the request comes in. 

  5. There are definitely cases in which you’d want to check for the resource but not abort if it’s missing, and it’d be easy to build a version for that behavior. I’ll touch on that more in a future post.