Adam spends all day every day alone with Eve and still has no clue what is in their household pantry. (5:313-328).
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.
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).
Phoenix doesn't have before_actions, though, and Elixir doesn't have state (no memoization via ||=). So, what do we do?
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.
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.
# cookies or stored values, triggering responses, etc.
That's some code! What does it all do?
# a simple delete method as an example.
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.
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.
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.
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.
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.
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.